fleetwatcher 0.4.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 (37) hide show
  1. fleetwatcher-0.4.0/LICENSE +21 -0
  2. fleetwatcher-0.4.0/PKG-INFO +200 -0
  3. fleetwatcher-0.4.0/README.md +173 -0
  4. fleetwatcher-0.4.0/pyproject.toml +49 -0
  5. fleetwatcher-0.4.0/setup.cfg +4 -0
  6. fleetwatcher-0.4.0/src/fleetwatch/__init__.py +3 -0
  7. fleetwatcher-0.4.0/src/fleetwatch/adapters/__init__.py +36 -0
  8. fleetwatcher-0.4.0/src/fleetwatch/adapters/base.py +112 -0
  9. fleetwatcher-0.4.0/src/fleetwatch/adapters/claude.py +425 -0
  10. fleetwatcher-0.4.0/src/fleetwatch/adapters/codex.py +411 -0
  11. fleetwatcher-0.4.0/src/fleetwatch/adapters/gemini.py +377 -0
  12. fleetwatcher-0.4.0/src/fleetwatch/adapters/grok.py +491 -0
  13. fleetwatcher-0.4.0/src/fleetwatch/cli.py +83 -0
  14. fleetwatcher-0.4.0/src/fleetwatch/config.py +43 -0
  15. fleetwatcher-0.4.0/src/fleetwatch/core.py +241 -0
  16. fleetwatcher-0.4.0/src/fleetwatch/models.py +110 -0
  17. fleetwatcher-0.4.0/src/fleetwatch/palette.py +107 -0
  18. fleetwatcher-0.4.0/src/fleetwatch/remote.py +104 -0
  19. fleetwatcher-0.4.0/src/fleetwatch/render.py +157 -0
  20. fleetwatcher-0.4.0/src/fleetwatch/summarize.py +141 -0
  21. fleetwatcher-0.4.0/src/fleetwatch/tailer.py +63 -0
  22. fleetwatcher-0.4.0/src/fleetwatch/tui.py +475 -0
  23. fleetwatcher-0.4.0/src/fleetwatch/util.py +28 -0
  24. fleetwatcher-0.4.0/src/fleetwatcher.egg-info/PKG-INFO +200 -0
  25. fleetwatcher-0.4.0/src/fleetwatcher.egg-info/SOURCES.txt +35 -0
  26. fleetwatcher-0.4.0/src/fleetwatcher.egg-info/dependency_links.txt +1 -0
  27. fleetwatcher-0.4.0/src/fleetwatcher.egg-info/entry_points.txt +3 -0
  28. fleetwatcher-0.4.0/src/fleetwatcher.egg-info/requires.txt +7 -0
  29. fleetwatcher-0.4.0/src/fleetwatcher.egg-info/top_level.txt +1 -0
  30. fleetwatcher-0.4.0/tests/test_claude.py +218 -0
  31. fleetwatcher-0.4.0/tests/test_codex.py +228 -0
  32. fleetwatcher-0.4.0/tests/test_gemini.py +263 -0
  33. fleetwatcher-0.4.0/tests/test_grok.py +296 -0
  34. fleetwatcher-0.4.0/tests/test_remote.py +131 -0
  35. fleetwatcher-0.4.0/tests/test_render.py +76 -0
  36. fleetwatcher-0.4.0/tests/test_summarize.py +267 -0
  37. fleetwatcher-0.4.0/tests/test_tui.py +245 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luke Steuber
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,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: fleetwatcher
3
+ Version: 0.4.0
4
+ Summary: One screen for every terminal coding session you have running, across Claude Code, Codex, Grok, and Gemini
5
+ Author-email: Luke Steuber <luke@lukesteuber.com>
6
+ Project-URL: Homepage, https://github.com/lukeslp/fleetwatch
7
+ Project-URL: Repository, https://github.com/lukeslp/fleetwatch
8
+ Project-URL: Issues, https://github.com/lukeslp/fleetwatch/issues
9
+ Keywords: cli,tui,dashboard,claude-code,codex,coding-agents,monitoring
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Software Development
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: textual>=0.60
22
+ Provides-Extra: summaries
23
+ Requires-Dist: anthropic>=0.40; extra == "summaries"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # fleetwatch
29
+
30
+ One screen for every terminal coding session you have running.
31
+
32
+ fleetwatch watches the coding CLIs you already run (Claude Code, Codex, Grok,
33
+ Gemini) and shows a single live dashboard of what each session is doing and which
34
+ ones are waiting on you. It reads each tool's own session files on disk,
35
+ read-only. There is nothing to install into those tools, no daemon, no hooks. If
36
+ a CLI writes a transcript, fleetwatch can watch it.
37
+
38
+ ```
39
+ fleetwatch 17:18:43 active 1 waiting 1 idle 2 done 7 (total 11)
40
+
41
+ HOST VENDOR PROJECT STATE IDLE ! WHAT
42
+ local claude orrery active 3s editing PhaseSpaceCoordinator.swift
43
+ local claude bipolar waiting 40s ! waiting for you to approve: rm -rf build
44
+ dreamer codex storyblocks idle 4m finished a turn
45
+ dreamer gemini hivescape done 2h responding
46
+ ```
47
+
48
+ Status: early, but in daily use across a local Mac and a VPS. Current release
49
+ `0.3.x`.
50
+
51
+ ## What it tracks
52
+
53
+ For each session: the vendor, the project, what it is working on right now, how
54
+ long since it last moved, and a flag the moment it needs you.
55
+
56
+ | State | Meaning |
57
+ |-------|---------|
58
+ | `active` | the transcript just changed; it is working |
59
+ | `waiting` | blocked on you (a pending permission, an unanswered question) |
60
+ | `idle` | finished its turn a moment ago |
61
+ | `done` | finished a while ago |
62
+ | `error` | something failed, or the transcript cannot be read |
63
+
64
+ The `waiting` signal is the point of the whole tool, and it is built on the real
65
+ shape of each vendor's files: a Claude `tool_use` with no matching result, a
66
+ Codex command awaiting approval, a Grok `permission_requested` event with no
67
+ resolution. A session that is genuinely mid-tool stays `active`; only a stalled
68
+ one becomes `waiting`.
69
+
70
+ Each state has its own bright primary (blue for `active`, yellow for `waiting`,
71
+ red for `error`) plus a glyph (`● ◆ ✗ ○ ·`), so the board reads by shape even
72
+ with the color off. `idle` and `done` recede into grey. Vendors carry their own
73
+ accent (orange, cyan, magenta, violet), kept clear of the state colors so a
74
+ vendor tag never reads as a status. Nothing leans on a red-versus-green
75
+ distinction, the one pair color-blind readers cannot separate. The dashboard is
76
+ always in color; `--once` adds color when it prints to a terminal and stays
77
+ plain text when you pipe it to a file or a log.
78
+
79
+ ## Install
80
+
81
+ Not on PyPI yet. Install from source:
82
+
83
+ ```sh
84
+ git clone https://github.com/lukeslp/fleetwatch
85
+ cd fleetwatch
86
+ python3 -m venv .venv
87
+ .venv/bin/pip install -e . # dashboard only: free, no network
88
+ .venv/bin/pip install -e ".[summaries]" # add plain-language summaries (Claude Haiku)
89
+ ```
90
+
91
+ Python 3.10 or newer. The install puts `fleetwatch` and `fw` on the PATH inside
92
+ the venv; activate the venv, or symlink `.venv/bin/fleetwatch` onto your PATH.
93
+
94
+ ## Usage
95
+
96
+ ```sh
97
+ fleetwatch # live dashboard
98
+ fleetwatch --once # one text snapshot, then exit
99
+ fleetwatch --export-json # machine-readable snapshot (scripting, remote hosts)
100
+ fleetwatch --no-model # heuristics only, no network
101
+ fleetwatch --vendors claude,codex # watch a subset
102
+ fleetwatch --hosts dreamer=user@host # also watch another machine over ssh
103
+ fleetwatch --export-json --summarize-all # full report: summarize every session
104
+ ```
105
+
106
+ In the dashboard: `q` quit, `r` refresh now, `s` summarize the selected session,
107
+ `S` summarize the whole fleet, arrows or `j`/`k` to move. Selecting a session
108
+ also summarizes it on its own, and the detail panel shows the summary, the plan,
109
+ and the last exchange for whichever session is selected.
110
+
111
+ ## Supported CLIs
112
+
113
+ | CLI | Reads |
114
+ |-----|-------|
115
+ | Claude Code | `~/.claude/projects/<cwd>/<uuid>.jsonl` |
116
+ | Codex | `~/.codex/sessions/**/rollout-*.jsonl` |
117
+ | Grok | `~/.grok/sessions/<cwd>/` (history + per-session events) |
118
+ | Gemini | `~/.gemini/tmp/<project>/chats/*.jsonl` |
119
+
120
+ Gemini CLI has no human-approval gate, so its sessions report active, idle, and
121
+ done but never `waiting`. There is no on-disk "blocked on you" signal to read.
122
+
123
+ Each adapter is small and isolated, so adding a vendor is a contained job. See
124
+ [CONTRIBUTING.md](CONTRIBUTING.md).
125
+
126
+ ## Summaries (optional)
127
+
128
+ Every row carries a quick heuristic line for free. On top of that, fleetwatch can
129
+ write a one-sentence plain-language status with Claude Haiku.
130
+
131
+ Summaries turn on automatically when the `summaries` extra is installed and
132
+ `ANTHROPIC_API_KEY` is set. They run for sessions that need attention and for
133
+ whichever session you select, in the background, cached per session, so a fleet
134
+ you are not looking at costs nothing. Press `S` (or run `--summarize-all`) to
135
+ sweep every session at once.
136
+
137
+ Without the extra or the key, fleetwatch never makes a network call and shows the
138
+ heuristic line instead. `--no-model` forces heuristics-only even when summaries
139
+ are available.
140
+
141
+ ## Watching other machines
142
+
143
+ `fleetwatch --hosts dreamer=user@host` adds a remote host. Each refresh runs one
144
+ `ssh <host> fleetwatch --export-json`, so the remote normalizes its own sessions
145
+ and hands back the result: one command per host, the conversation content stays
146
+ on the channel you already trust, and a host that goes unreachable goes stale
147
+ rather than vanishing from the board. The remote just needs `fleetwatch` on its
148
+ `PATH`. Once more than one host is in view, a `HOST` column appears so you can
149
+ tell which machine each session is on.
150
+
151
+ ## Privacy
152
+
153
+ fleetwatch reads your CLIs' session files read-only and never writes to them. The
154
+ dashboard displays short excerpts (the last user and agent messages, the plan)
155
+ locally.
156
+
157
+ A summary is the only thing that ever leaves your machine, and only when you have
158
+ opted into summaries (the `summaries` extra plus `ANTHROPIC_API_KEY`): a short
159
+ slice of one session's recent activity is sent to Claude Haiku to write a single
160
+ sentence. With `--no-model`, or without the extra or key, nothing is sent
161
+ anywhere. Remote hosts are read over your own ssh connection; their session
162
+ content travels only over that channel.
163
+
164
+ ## How it works
165
+
166
+ Four small layers, each independently testable:
167
+
168
+ 1. **Adapters** translate one vendor's files into a single normalized
169
+ `SessionState`. One adapter per vendor, each tested against captured fixture
170
+ transcripts.
171
+ 2. **The aggregator** polls adapters by file modification time and reads only a
172
+ bounded tail of each transcript, so a 25 MB session file is never read whole.
173
+ It sorts the fleet needs-first and merges remote hosts.
174
+ 3. **The summarizer** adds the Haiku sentence, cached and off the UI thread.
175
+ 4. **The dashboard** (Textual) renders it and refreshes on an interval.
176
+
177
+ ## Configuration
178
+
179
+ All optional, via environment variables:
180
+
181
+ | Variable | Default | Effect |
182
+ |----------|---------|--------|
183
+ | `FLEETWATCH_ACTIVE_WINDOW` | `12` | seconds of quiet before a session stops counting as active |
184
+ | `FLEETWATCH_DONE_AFTER` | `1800` | seconds of quiet before idle becomes done |
185
+ | `FLEETWATCH_MAX_AGE` | `259200` | drop sessions older than this (3 days) |
186
+ | `FLEETWATCH_REFRESH` | `2` | dashboard refresh interval, seconds |
187
+ | `FLEETWATCH_MODEL` | `claude-haiku-4-5-20251001` | summary model |
188
+ | `FLEETWATCH_VENDORS` | `claude,codex,grok,gemini` | which CLIs to watch |
189
+ | `FLEETWATCH_HOSTS` | unset | remote hosts over ssh (`name` or `name=ssh_target`, comma-separated) |
190
+ | `FLEETWATCH_NO_MODEL` | unset | set to `1` to disable summaries |
191
+
192
+ ## Contributing
193
+
194
+ Bug reports, vendor adapters, and fixes are welcome. See
195
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the setup, the adapter contract, and how
196
+ to add a new CLI.
197
+
198
+ ## License
199
+
200
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,173 @@
1
+ # fleetwatch
2
+
3
+ One screen for every terminal coding session you have running.
4
+
5
+ fleetwatch watches the coding CLIs you already run (Claude Code, Codex, Grok,
6
+ Gemini) and shows a single live dashboard of what each session is doing and which
7
+ ones are waiting on you. It reads each tool's own session files on disk,
8
+ read-only. There is nothing to install into those tools, no daemon, no hooks. If
9
+ a CLI writes a transcript, fleetwatch can watch it.
10
+
11
+ ```
12
+ fleetwatch 17:18:43 active 1 waiting 1 idle 2 done 7 (total 11)
13
+
14
+ HOST VENDOR PROJECT STATE IDLE ! WHAT
15
+ local claude orrery active 3s editing PhaseSpaceCoordinator.swift
16
+ local claude bipolar waiting 40s ! waiting for you to approve: rm -rf build
17
+ dreamer codex storyblocks idle 4m finished a turn
18
+ dreamer gemini hivescape done 2h responding
19
+ ```
20
+
21
+ Status: early, but in daily use across a local Mac and a VPS. Current release
22
+ `0.3.x`.
23
+
24
+ ## What it tracks
25
+
26
+ For each session: the vendor, the project, what it is working on right now, how
27
+ long since it last moved, and a flag the moment it needs you.
28
+
29
+ | State | Meaning |
30
+ |-------|---------|
31
+ | `active` | the transcript just changed; it is working |
32
+ | `waiting` | blocked on you (a pending permission, an unanswered question) |
33
+ | `idle` | finished its turn a moment ago |
34
+ | `done` | finished a while ago |
35
+ | `error` | something failed, or the transcript cannot be read |
36
+
37
+ The `waiting` signal is the point of the whole tool, and it is built on the real
38
+ shape of each vendor's files: a Claude `tool_use` with no matching result, a
39
+ Codex command awaiting approval, a Grok `permission_requested` event with no
40
+ resolution. A session that is genuinely mid-tool stays `active`; only a stalled
41
+ one becomes `waiting`.
42
+
43
+ Each state has its own bright primary (blue for `active`, yellow for `waiting`,
44
+ red for `error`) plus a glyph (`● ◆ ✗ ○ ·`), so the board reads by shape even
45
+ with the color off. `idle` and `done` recede into grey. Vendors carry their own
46
+ accent (orange, cyan, magenta, violet), kept clear of the state colors so a
47
+ vendor tag never reads as a status. Nothing leans on a red-versus-green
48
+ distinction, the one pair color-blind readers cannot separate. The dashboard is
49
+ always in color; `--once` adds color when it prints to a terminal and stays
50
+ plain text when you pipe it to a file or a log.
51
+
52
+ ## Install
53
+
54
+ Not on PyPI yet. Install from source:
55
+
56
+ ```sh
57
+ git clone https://github.com/lukeslp/fleetwatch
58
+ cd fleetwatch
59
+ python3 -m venv .venv
60
+ .venv/bin/pip install -e . # dashboard only: free, no network
61
+ .venv/bin/pip install -e ".[summaries]" # add plain-language summaries (Claude Haiku)
62
+ ```
63
+
64
+ Python 3.10 or newer. The install puts `fleetwatch` and `fw` on the PATH inside
65
+ the venv; activate the venv, or symlink `.venv/bin/fleetwatch` onto your PATH.
66
+
67
+ ## Usage
68
+
69
+ ```sh
70
+ fleetwatch # live dashboard
71
+ fleetwatch --once # one text snapshot, then exit
72
+ fleetwatch --export-json # machine-readable snapshot (scripting, remote hosts)
73
+ fleetwatch --no-model # heuristics only, no network
74
+ fleetwatch --vendors claude,codex # watch a subset
75
+ fleetwatch --hosts dreamer=user@host # also watch another machine over ssh
76
+ fleetwatch --export-json --summarize-all # full report: summarize every session
77
+ ```
78
+
79
+ In the dashboard: `q` quit, `r` refresh now, `s` summarize the selected session,
80
+ `S` summarize the whole fleet, arrows or `j`/`k` to move. Selecting a session
81
+ also summarizes it on its own, and the detail panel shows the summary, the plan,
82
+ and the last exchange for whichever session is selected.
83
+
84
+ ## Supported CLIs
85
+
86
+ | CLI | Reads |
87
+ |-----|-------|
88
+ | Claude Code | `~/.claude/projects/<cwd>/<uuid>.jsonl` |
89
+ | Codex | `~/.codex/sessions/**/rollout-*.jsonl` |
90
+ | Grok | `~/.grok/sessions/<cwd>/` (history + per-session events) |
91
+ | Gemini | `~/.gemini/tmp/<project>/chats/*.jsonl` |
92
+
93
+ Gemini CLI has no human-approval gate, so its sessions report active, idle, and
94
+ done but never `waiting`. There is no on-disk "blocked on you" signal to read.
95
+
96
+ Each adapter is small and isolated, so adding a vendor is a contained job. See
97
+ [CONTRIBUTING.md](CONTRIBUTING.md).
98
+
99
+ ## Summaries (optional)
100
+
101
+ Every row carries a quick heuristic line for free. On top of that, fleetwatch can
102
+ write a one-sentence plain-language status with Claude Haiku.
103
+
104
+ Summaries turn on automatically when the `summaries` extra is installed and
105
+ `ANTHROPIC_API_KEY` is set. They run for sessions that need attention and for
106
+ whichever session you select, in the background, cached per session, so a fleet
107
+ you are not looking at costs nothing. Press `S` (or run `--summarize-all`) to
108
+ sweep every session at once.
109
+
110
+ Without the extra or the key, fleetwatch never makes a network call and shows the
111
+ heuristic line instead. `--no-model` forces heuristics-only even when summaries
112
+ are available.
113
+
114
+ ## Watching other machines
115
+
116
+ `fleetwatch --hosts dreamer=user@host` adds a remote host. Each refresh runs one
117
+ `ssh <host> fleetwatch --export-json`, so the remote normalizes its own sessions
118
+ and hands back the result: one command per host, the conversation content stays
119
+ on the channel you already trust, and a host that goes unreachable goes stale
120
+ rather than vanishing from the board. The remote just needs `fleetwatch` on its
121
+ `PATH`. Once more than one host is in view, a `HOST` column appears so you can
122
+ tell which machine each session is on.
123
+
124
+ ## Privacy
125
+
126
+ fleetwatch reads your CLIs' session files read-only and never writes to them. The
127
+ dashboard displays short excerpts (the last user and agent messages, the plan)
128
+ locally.
129
+
130
+ A summary is the only thing that ever leaves your machine, and only when you have
131
+ opted into summaries (the `summaries` extra plus `ANTHROPIC_API_KEY`): a short
132
+ slice of one session's recent activity is sent to Claude Haiku to write a single
133
+ sentence. With `--no-model`, or without the extra or key, nothing is sent
134
+ anywhere. Remote hosts are read over your own ssh connection; their session
135
+ content travels only over that channel.
136
+
137
+ ## How it works
138
+
139
+ Four small layers, each independently testable:
140
+
141
+ 1. **Adapters** translate one vendor's files into a single normalized
142
+ `SessionState`. One adapter per vendor, each tested against captured fixture
143
+ transcripts.
144
+ 2. **The aggregator** polls adapters by file modification time and reads only a
145
+ bounded tail of each transcript, so a 25 MB session file is never read whole.
146
+ It sorts the fleet needs-first and merges remote hosts.
147
+ 3. **The summarizer** adds the Haiku sentence, cached and off the UI thread.
148
+ 4. **The dashboard** (Textual) renders it and refreshes on an interval.
149
+
150
+ ## Configuration
151
+
152
+ All optional, via environment variables:
153
+
154
+ | Variable | Default | Effect |
155
+ |----------|---------|--------|
156
+ | `FLEETWATCH_ACTIVE_WINDOW` | `12` | seconds of quiet before a session stops counting as active |
157
+ | `FLEETWATCH_DONE_AFTER` | `1800` | seconds of quiet before idle becomes done |
158
+ | `FLEETWATCH_MAX_AGE` | `259200` | drop sessions older than this (3 days) |
159
+ | `FLEETWATCH_REFRESH` | `2` | dashboard refresh interval, seconds |
160
+ | `FLEETWATCH_MODEL` | `claude-haiku-4-5-20251001` | summary model |
161
+ | `FLEETWATCH_VENDORS` | `claude,codex,grok,gemini` | which CLIs to watch |
162
+ | `FLEETWATCH_HOSTS` | unset | remote hosts over ssh (`name` or `name=ssh_target`, comma-separated) |
163
+ | `FLEETWATCH_NO_MODEL` | unset | set to `1` to disable summaries |
164
+
165
+ ## Contributing
166
+
167
+ Bug reports, vendor adapters, and fixes are welcome. See
168
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the setup, the adapter contract, and how
169
+ to add a new CLI.
170
+
171
+ ## License
172
+
173
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # Distribution name on PyPI. The import package and the CLI commands are both
7
+ # "fleetwatch"; the dist name differs because "fleetwatch" was too similar to an
8
+ # existing project ("fleet-watch") for PyPI to accept.
9
+ name = "fleetwatcher"
10
+ version = "0.4.0"
11
+ description = "One screen for every terminal coding session you have running, across Claude Code, Codex, Grok, and Gemini"
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ authors = [{ name = "Luke Steuber", email = "luke@lukesteuber.com" }]
15
+ keywords = ["cli", "tui", "dashboard", "claude-code", "codex", "coding-agents", "monitoring"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Topic :: Software Development",
24
+ "Topic :: Utilities",
25
+ ]
26
+ dependencies = [
27
+ "textual>=0.60",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ # Plain-language summaries call Claude Haiku. Kept optional so a bare install
32
+ # never pulls the SDK or spends tokens; opt in with: pip install "fleetwatch[summaries]"
33
+ summaries = ["anthropic>=0.40"]
34
+ dev = ["pytest>=8"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/lukeslp/fleetwatch"
38
+ Repository = "https://github.com/lukeslp/fleetwatch"
39
+ Issues = "https://github.com/lukeslp/fleetwatch/issues"
40
+
41
+ [project.scripts]
42
+ fleetwatch = "fleetwatch.cli:main"
43
+ fw = "fleetwatch.cli:main"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """fleetwatch — one screen for every terminal coding session you're running."""
2
+
3
+ __version__ = "0.3.1"
@@ -0,0 +1,36 @@
1
+ """Vendor adapters. ``all_adapters()`` returns the enabled, importable set."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+
8
+ from ..config import ENABLED_VENDORS
9
+ from .base import Adapter, LocalSource, SessionRef, Source
10
+
11
+ _REGISTRY = {
12
+ "claude": ("claude", "ClaudeAdapter"),
13
+ "codex": ("codex", "CodexAdapter"),
14
+ "grok": ("grok", "GrokAdapter"),
15
+ "gemini": ("gemini", "GeminiAdapter"),
16
+ }
17
+
18
+
19
+ def all_adapters() -> list[Adapter]:
20
+ """Instantiate every enabled adapter. An adapter that fails to import is
21
+ skipped with a warning rather than taking the whole tool down."""
22
+ out: list[Adapter] = []
23
+ for vendor in ENABLED_VENDORS:
24
+ spec = _REGISTRY.get(vendor)
25
+ if not spec:
26
+ continue
27
+ module_name, class_name = spec
28
+ try:
29
+ mod = importlib.import_module(f".{module_name}", __package__)
30
+ out.append(getattr(mod, class_name)())
31
+ except Exception as exc: # never let one adapter break the others
32
+ print(f"fleetwatch: skipping {vendor} adapter ({exc})", file=sys.stderr)
33
+ return out
34
+
35
+
36
+ __all__ = ["Adapter", "SessionRef", "Source", "LocalSource", "all_adapters"]
@@ -0,0 +1,112 @@
1
+ """The adapter contract and the local-filesystem Source.
2
+
3
+ Adapters never touch the filesystem directly; they go through a ``Source``.
4
+ Today the only Source is ``LocalSource``. A future ``RemoteSource`` (SSH, or a
5
+ pushed JSON export from each host) implements the same surface, so adapters work
6
+ unchanged against VPS sessions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import glob as _glob
12
+ import os
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass
15
+ from typing import Optional
16
+
17
+ from ..models import SessionState
18
+ from ..tailer import read_tail_lines, read_tail_records
19
+
20
+
21
+ @dataclass
22
+ class SessionRef:
23
+ """A pointer to one session's primary file, returned by ``discover()``."""
24
+
25
+ path: str # the primary transcript / history file
26
+ session_id: str
27
+ cwd: Optional[str] = None # decoded working directory when known
28
+ mtime: Optional[float] = None # freshness hint: newest mtime across a
29
+ # multi-file session. When set, the aggregator
30
+ # uses it instead of the primary file's mtime,
31
+ # so a change in any of the session's files
32
+ # (e.g. Grok's events.jsonl) is not missed.
33
+
34
+
35
+ class Source(ABC):
36
+ """Abstracts where session files live so the same adapters can read a remote
37
+ host later. Implementations must never raise on missing files."""
38
+
39
+ host: str = "local"
40
+
41
+ @abstractmethod
42
+ def expand(self, path: str) -> str: ...
43
+ @abstractmethod
44
+ def exists(self, path: str) -> bool: ...
45
+ @abstractmethod
46
+ def glob(self, pattern: str) -> list[str]: ...
47
+ @abstractmethod
48
+ def mtime(self, path: str) -> float: ...
49
+ @abstractmethod
50
+ def read_text(self, path: str) -> str: ...
51
+ @abstractmethod
52
+ def tail_lines(self, path: str, max_bytes: int = 512_000) -> list[str]: ...
53
+ @abstractmethod
54
+ def tail_records(self, path: str, max_bytes: int = 512_000) -> list[dict]: ...
55
+
56
+
57
+ class LocalSource(Source):
58
+ host = "local"
59
+
60
+ def expand(self, path: str) -> str:
61
+ return os.path.expanduser(os.path.expandvars(path))
62
+
63
+ def exists(self, path: str) -> bool:
64
+ return os.path.exists(self.expand(path))
65
+
66
+ def glob(self, pattern: str) -> list[str]:
67
+ return _glob.glob(self.expand(pattern))
68
+
69
+ def mtime(self, path: str) -> float:
70
+ try:
71
+ return os.path.getmtime(self.expand(path))
72
+ except OSError:
73
+ return 0.0
74
+
75
+ def read_text(self, path: str) -> str:
76
+ try:
77
+ with open(self.expand(path), "r", encoding="utf-8", errors="replace") as fh:
78
+ return fh.read()
79
+ except OSError:
80
+ return ""
81
+
82
+ def tail_lines(self, path: str, max_bytes: int = 512_000) -> list[str]:
83
+ return read_tail_lines(self.expand(path), max_bytes)
84
+
85
+ def tail_records(self, path: str, max_bytes: int = 512_000) -> list[dict]:
86
+ return read_tail_records(self.expand(path), max_bytes)
87
+
88
+
89
+ class Adapter(ABC):
90
+ """One adapter per vendor. Pure translation: vendor files in, SessionState out."""
91
+
92
+ vendor: str = "unknown"
93
+
94
+ @abstractmethod
95
+ def discover(self, source: Source) -> list[SessionRef]:
96
+ """Find candidate sessions for this vendor. Must be cheap — it runs every
97
+ refresh. Return ``[]`` when the vendor is not installed."""
98
+ ...
99
+
100
+ @abstractmethod
101
+ def read(
102
+ self, source: Source, ref: SessionRef, prev: Optional[SessionState]
103
+ ) -> Optional[SessionState]:
104
+ """Produce the current SessionState for one ref.
105
+
106
+ ``prev`` is the last state held for this session (or ``None`` on first
107
+ sight). Adapters may use it to carry context forward but must stay
108
+ correct when it is ``None``. Return ``None`` to skip a ref that is not
109
+ really a session. This method must not raise: on trouble, return a
110
+ SessionState with ``state=State.ERROR`` and a message in ``error``.
111
+ """
112
+ ...