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.
- fleetwatcher-0.4.0/LICENSE +21 -0
- fleetwatcher-0.4.0/PKG-INFO +200 -0
- fleetwatcher-0.4.0/README.md +173 -0
- fleetwatcher-0.4.0/pyproject.toml +49 -0
- fleetwatcher-0.4.0/setup.cfg +4 -0
- fleetwatcher-0.4.0/src/fleetwatch/__init__.py +3 -0
- fleetwatcher-0.4.0/src/fleetwatch/adapters/__init__.py +36 -0
- fleetwatcher-0.4.0/src/fleetwatch/adapters/base.py +112 -0
- fleetwatcher-0.4.0/src/fleetwatch/adapters/claude.py +425 -0
- fleetwatcher-0.4.0/src/fleetwatch/adapters/codex.py +411 -0
- fleetwatcher-0.4.0/src/fleetwatch/adapters/gemini.py +377 -0
- fleetwatcher-0.4.0/src/fleetwatch/adapters/grok.py +491 -0
- fleetwatcher-0.4.0/src/fleetwatch/cli.py +83 -0
- fleetwatcher-0.4.0/src/fleetwatch/config.py +43 -0
- fleetwatcher-0.4.0/src/fleetwatch/core.py +241 -0
- fleetwatcher-0.4.0/src/fleetwatch/models.py +110 -0
- fleetwatcher-0.4.0/src/fleetwatch/palette.py +107 -0
- fleetwatcher-0.4.0/src/fleetwatch/remote.py +104 -0
- fleetwatcher-0.4.0/src/fleetwatch/render.py +157 -0
- fleetwatcher-0.4.0/src/fleetwatch/summarize.py +141 -0
- fleetwatcher-0.4.0/src/fleetwatch/tailer.py +63 -0
- fleetwatcher-0.4.0/src/fleetwatch/tui.py +475 -0
- fleetwatcher-0.4.0/src/fleetwatch/util.py +28 -0
- fleetwatcher-0.4.0/src/fleetwatcher.egg-info/PKG-INFO +200 -0
- fleetwatcher-0.4.0/src/fleetwatcher.egg-info/SOURCES.txt +35 -0
- fleetwatcher-0.4.0/src/fleetwatcher.egg-info/dependency_links.txt +1 -0
- fleetwatcher-0.4.0/src/fleetwatcher.egg-info/entry_points.txt +3 -0
- fleetwatcher-0.4.0/src/fleetwatcher.egg-info/requires.txt +7 -0
- fleetwatcher-0.4.0/src/fleetwatcher.egg-info/top_level.txt +1 -0
- fleetwatcher-0.4.0/tests/test_claude.py +218 -0
- fleetwatcher-0.4.0/tests/test_codex.py +228 -0
- fleetwatcher-0.4.0/tests/test_gemini.py +263 -0
- fleetwatcher-0.4.0/tests/test_grok.py +296 -0
- fleetwatcher-0.4.0/tests/test_remote.py +131 -0
- fleetwatcher-0.4.0/tests/test_render.py +76 -0
- fleetwatcher-0.4.0/tests/test_summarize.py +267 -0
- 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,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
|
+
...
|