portlist-tui 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.
- portlist_tui-1.2/LICENSE +21 -0
- portlist_tui-1.2/PKG-INFO +540 -0
- portlist_tui-1.2/README.md +491 -0
- portlist_tui-1.2/plcore/__init__.py +2 -0
- portlist_tui-1.2/plcore/__main__.py +7 -0
- portlist_tui-1.2/plcore/access.py +349 -0
- portlist_tui-1.2/plcore/activity.py +188 -0
- portlist_tui-1.2/plcore/agents.py +184 -0
- portlist_tui-1.2/plcore/app.py +105 -0
- portlist_tui-1.2/plcore/catalog.py +179 -0
- portlist_tui-1.2/plcore/collect.py +87 -0
- portlist_tui-1.2/plcore/containers.py +228 -0
- portlist_tui-1.2/plcore/dash.py +619 -0
- portlist_tui-1.2/plcore/data/room.png +0 -0
- portlist_tui-1.2/plcore/depends.py +136 -0
- portlist_tui-1.2/plcore/deps.py +68 -0
- portlist_tui-1.2/plcore/fingerprint.py +381 -0
- portlist_tui-1.2/plcore/fix.py +282 -0
- portlist_tui-1.2/plcore/freeport.py +197 -0
- portlist_tui-1.2/plcore/gitinfo.py +84 -0
- portlist_tui-1.2/plcore/graph.py +356 -0
- portlist_tui-1.2/plcore/graphview.py +230 -0
- portlist_tui-1.2/plcore/history.py +137 -0
- portlist_tui-1.2/plcore/imgmap.py +192 -0
- portlist_tui-1.2/plcore/ledger.py +532 -0
- portlist_tui-1.2/plcore/lifecycle.py +388 -0
- portlist_tui-1.2/plcore/mcp.py +376 -0
- portlist_tui-1.2/plcore/mcpclients.py +333 -0
- portlist_tui-1.2/plcore/names.py +120 -0
- portlist_tui-1.2/plcore/platforms/__init__.py +19 -0
- portlist_tui-1.2/plcore/platforms/base.py +145 -0
- portlist_tui-1.2/plcore/platforms/darwin.py +360 -0
- portlist_tui-1.2/plcore/platforms/linux.py +533 -0
- portlist_tui-1.2/plcore/platforms/windows.py +247 -0
- portlist_tui-1.2/plcore/posture.py +281 -0
- portlist_tui-1.2/plcore/projects.py +140 -0
- portlist_tui-1.2/plcore/provenance.py +345 -0
- portlist_tui-1.2/plcore/recipes.py +248 -0
- portlist_tui-1.2/plcore/remote.py +176 -0
- portlist_tui-1.2/plcore/risk.py +119 -0
- portlist_tui-1.2/plcore/scan.py +1141 -0
- portlist_tui-1.2/plcore/security.py +271 -0
- portlist_tui-1.2/plcore/sessions.py +561 -0
- portlist_tui-1.2/plcore/termimg.py +134 -0
- portlist_tui-1.2/plcore/tui.py +1527 -0
- portlist_tui-1.2/plcore/vibe.py +1025 -0
- portlist_tui-1.2/portlist_tui.egg-info/PKG-INFO +540 -0
- portlist_tui-1.2/portlist_tui.egg-info/SOURCES.txt +53 -0
- portlist_tui-1.2/portlist_tui.egg-info/dependency_links.txt +1 -0
- portlist_tui-1.2/portlist_tui.egg-info/entry_points.txt +2 -0
- portlist_tui-1.2/portlist_tui.egg-info/requires.txt +3 -0
- portlist_tui-1.2/portlist_tui.egg-info/top_level.txt +1 -0
- portlist_tui-1.2/pyproject.toml +51 -0
- portlist_tui-1.2/setup.cfg +4 -0
- portlist_tui-1.2/tests/test_ledger.py +391 -0
portlist_tui-1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 portlist contributors
|
|
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,540 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: portlist-tui
|
|
3
|
+
Version: 1.2
|
|
4
|
+
Summary: Every port on this machine, and where it came from. A terminal program.
|
|
5
|
+
Author: Portlist contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 portlist contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://mr-hunt-007.github.io/portlist/
|
|
29
|
+
Project-URL: Source, https://github.com/Mr-hunt-007/portlist
|
|
30
|
+
Project-URL: Issues, https://github.com/Mr-hunt-007/portlist/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/Mr-hunt-007/portlist/blob/main/CHANGELOG.md
|
|
32
|
+
Keywords: ports,netstat,lsof,tui,terminal,devtools,localhost,developer-tools,process-monitor,curses
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Environment :: Console :: Curses
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: Intended Audience :: System Administrators
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
39
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
40
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
41
|
+
Classifier: Programming Language :: Python :: 3
|
|
42
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
43
|
+
Classifier: Topic :: System :: Systems Administration
|
|
44
|
+
Requires-Python: >=3.9
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Requires-Dist: windows-curses>=2.3; sys_platform == "win32"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
<h1 align="center">portlist</h1>
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
<strong>Every port on this machine, and where it came from.</strong><br>
|
|
54
|
+
A terminal program. No web UI, no dashboard, no server, no dependencies.
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<p align="center">
|
|
58
|
+
<a href="https://mr-hunt-007.github.io/portlist/">Website</a> ·
|
|
59
|
+
<a href="docs/USAGE.md">Usage</a> ·
|
|
60
|
+
<a href="CHANGELOG.md">Changelog</a> ·
|
|
61
|
+
<a href="https://github.com/Mr-hunt-007/portlist/issues">Issues</a> ·
|
|
62
|
+
<a href="LICENSE">MIT</a> ·
|
|
63
|
+
<a href="https://buymeacoffee.com/mr.hunt.007">Buy me a coffee</a>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
PORTLIST devbox 12 listening 2 off-box 2 need attention 21:20:37
|
|
68
|
+
1 Services(12) 2 Exposed(2) 3 Attention(2) 4 Leftovers(5) 5 Agents(3) 6 Containers(0) 7 Sessions(8) 8 System
|
|
69
|
+
|
|
70
|
+
PORT SERVICE PROJECT REACHABLE RISK STARTED BY
|
|
71
|
+
:3000 Next.js storefront Localhost only 12 Info Claude Code 2h
|
|
72
|
+
:5173 Vite admin-ui Localhost only 12 Info terminal 5h
|
|
73
|
+
:8787 Python http.server data-export All interfaces 71 High Claude Code 5d
|
|
74
|
+
:9050 Tor /opt/homebrew Localhost only 12 Info launchd 24d
|
|
75
|
+
|
|
76
|
+
j/k move h/l pane tab view enter detail O open / search f free port V vibe q quit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Twelve things are listening. You started three of them today and you cannot name
|
|
80
|
+
the rest.
|
|
81
|
+
|
|
82
|
+
## What problem does it solve
|
|
83
|
+
|
|
84
|
+
`lsof -i` and `netstat` answer *what is bound to this port*. That was the whole
|
|
85
|
+
question when a machine ran two servers you started by hand. It is not the
|
|
86
|
+
question any more, because most of what is listening on a developer laptop was
|
|
87
|
+
started by something else: an agent session, an editor, a container, a service
|
|
88
|
+
manager, or you, on Tuesday, in a directory you have since forgotten.
|
|
89
|
+
|
|
90
|
+
The questions that are actually in the way:
|
|
91
|
+
|
|
92
|
+
| you ask | `lsof` says | portlist says |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| what is on :8787? | `Python, pid 96798` | a static file server, started by a Claude Code session, in `~/code/data-export` |
|
|
95
|
+
| who started it? | nothing | that session, and whether it has since exited |
|
|
96
|
+
| can anyone reach it? | a bind string like `0.0.0.0` | what answered when it connected to this machine's real address |
|
|
97
|
+
| is it still needed? | how long it has been *up* | how long it has been *unused*, measured over time |
|
|
98
|
+
| is it safe to kill? | nothing | what depends on it, what it depends on, and the command to stop it |
|
|
99
|
+
|
|
100
|
+
Three situations it was built for:
|
|
101
|
+
|
|
102
|
+
**"Address already in use."** Something holds :3000 and you did not start it.
|
|
103
|
+
Was it this morning's agent session, a container, or a dev server from last
|
|
104
|
+
Tuesday that never died? portlist names it, and if you just want a port that is
|
|
105
|
+
free now and not spoken for later, `f` gives you one.
|
|
106
|
+
|
|
107
|
+
**An agent left the lights on.** Coding agents start servers and move on. The
|
|
108
|
+
session exits, the server does not, and nothing on the machine remembers which
|
|
109
|
+
session it belonged to. portlist writes a launch record the first time it sees a
|
|
110
|
+
service and never rewrites it, so the answer survives both the agent exiting and
|
|
111
|
+
the service being restarted by something else.
|
|
112
|
+
|
|
113
|
+
**Bound to the world by accident.** `0.0.0.0` in a config file is a claim. The
|
|
114
|
+
only honest answer comes from connecting to this machine's real address and
|
|
115
|
+
seeing what answers, which is what the `REACHABLE` column is.
|
|
116
|
+
|
|
117
|
+
And the rule underneath all of it: **it never dresses a guess as a fact.** A
|
|
118
|
+
service that was already listening before portlist first looked has *unknown*
|
|
119
|
+
origin and says so, rather than inheriting a label from whatever owns the port
|
|
120
|
+
now. "The container engine did not answer" is never rendered as "no containers".
|
|
121
|
+
|
|
122
|
+
## Install
|
|
123
|
+
|
|
124
|
+
**Homebrew** (macOS, Linux)
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
brew tap Mr-hunt-007/portlist https://github.com/Mr-hunt-007/homebrew-portlist
|
|
128
|
+
brew trust mr-hunt-007/portlist # Homebrew asks this of every third-party tap
|
|
129
|
+
brew install portlist
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**pip or pipx**, anywhere. pipx is the simplest route on Windows, because it
|
|
133
|
+
brings `windows-curses` along:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
pipx install git+https://github.com/Mr-hunt-007/portlist
|
|
137
|
+
pip install git+https://github.com/Mr-hunt-007/portlist
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**One line**, into `~/.local`, no root and nothing system-wide:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
curl -fsSL https://mr-hunt-007.github.io/portlist/install.sh | sh
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**From source**, which needs nothing but Python:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
git clone https://github.com/Mr-hunt-007/portlist && cd portlist && python3 portlist.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then run `portlist`.
|
|
153
|
+
|
|
154
|
+
<sub>Not yet published: `pip install portlist-tui` (the artifacts are built and
|
|
155
|
+
pass `twine check`, but nothing has been uploaded) and `winget install` (the
|
|
156
|
+
manifests are in `packaging/winget/`, not submitted). The name `portlist` on
|
|
157
|
+
PyPI belongs to an unrelated package, which is why the distribution is
|
|
158
|
+
`portlist-tui` while the command stays `portlist`.</sub>
|
|
159
|
+
|
|
160
|
+
## The dashboard
|
|
161
|
+
|
|
162
|
+
`0`, and it is where portlist opens. Everything about the machine on one screen,
|
|
163
|
+
so a single screenshot tells the whole story.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
MACHINE EXPOSURE AGENTS CONTAINERS
|
|
167
|
+
· ● ● · ● ● ◉ ● ● LISTENING 16 Claude Code 14 ENGINE docker
|
|
168
|
+
· ● · ● ● ● EXPOSED 2 terminal 1 STATE no answer
|
|
169
|
+
· 36% ◉ ◉ 85% ● ● 96% ● NEEDS WORK 2 launchd 1 count unknown
|
|
170
|
+
· CPU · ● RAM ● ● DISK ● UNKNOWN ORIGIN 12 not zero
|
|
171
|
+
· · · ● ● ● ● ● ●
|
|
172
|
+
LOAD 3.56 3.37 3.64
|
|
173
|
+
|
|
174
|
+
LISTENING 14 services Tab next section
|
|
175
|
+
PORT SERVICE PROJECT STARTED BY REACH RISK
|
|
176
|
+
● :7337 Grafana metrics terminal Localhost only 12 Info
|
|
177
|
+
○ :8000 Python http.server analytics Claude Code Localhost only 12 Info
|
|
178
|
+
○ :8787 Python http.server data-export Claude Code All interfaces 71 High
|
|
179
|
+
|
|
180
|
+
SELECTED SERVICE │ ACTIVITY
|
|
181
|
+
│
|
|
182
|
+
Grafana :7337 │ 22:52:49 · Bun on :10065 stopped listening
|
|
183
|
+
~/code/metrics │ 22:23:58 · Bun opened on :57155 (loopback)
|
|
184
|
+
│
|
|
185
|
+
REACH Localhost only │ CPU
|
|
186
|
+
PID 9561 Python │ ▃▃▄▃▃▂▃▄▅▄▃▃▃
|
|
187
|
+
LAST USED in use now │ MEMORY
|
|
188
|
+
│ ▇▇▇▇▇▇▇▇▇▇▇▇▇
|
|
189
|
+
RISK 71 / 100 High │
|
|
190
|
+
+42 Listening on all interfaces (0.0.0.0) │
|
|
191
|
+
+10 No authentication seen and reachable off-box │
|
|
192
|
+
|
|
193
|
+
● 2 exposed ◆ 2 need attention ⚠ 12 unknown origin ◉ 3 agents firewall on
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Tab** moves to the next view, in the same order as the number keys, and
|
|
197
|
+
**shift-Tab** goes back. **`h`** and **`l`** (or the arrows) move between the
|
|
198
|
+
dashboard's panes, and `j`/`k` move inside whichever has focus.
|
|
199
|
+
|
|
200
|
+
The three dials are live: each ring is twelve segments, the leading one pulses,
|
|
201
|
+
and the ring eases round rather than jumping when the reading changes. An
|
|
202
|
+
unmeasured value draws an empty ring and says so instead of resting at zero.
|
|
203
|
+
|
|
204
|
+
Under every card and across the table runs the same travelling wave, and its
|
|
205
|
+
**amplitude and speed are the reading**: a quiet machine ripples slowly and
|
|
206
|
+
shallowly, a busy one moves faster and taller. It is a sine, not a history, and
|
|
207
|
+
it is never drawn where a history belongs; the sparklines plot real samples. A
|
|
208
|
+
card with nothing to measure draws a flat line of dots rather than a wave at
|
|
209
|
+
zero, because a flat wave still reads as a measurement of zero, and "not
|
|
210
|
+
measured" is not zero.
|
|
211
|
+
|
|
212
|
+
The risk score is never a bare number. The pane lists what it was made of, so
|
|
213
|
+
`71 High` is auditable rather than magical, and `⚠ 12 unknown origin` is counted
|
|
214
|
+
precisely so those services are not quietly attributed to whatever owns the port
|
|
215
|
+
now.
|
|
216
|
+
|
|
217
|
+
## Ten views
|
|
218
|
+
|
|
219
|
+
| key | view | what it answers |
|
|
220
|
+
|-----|------|-----------------|
|
|
221
|
+
| `0` | dashboard | everything at once, and where it opens |
|
|
222
|
+
| `1` | services | everything listening, with who started it |
|
|
223
|
+
| `2` | exposed | reachable from beyond this machine |
|
|
224
|
+
| `3` | attention | critical, high and medium risk |
|
|
225
|
+
| `4` | leftovers | looks abandoned, with the measurements behind the guess |
|
|
226
|
+
| `5` | agents | grouped by the agent, editor or terminal that started it |
|
|
227
|
+
| `6` | containers | by compose project, and the host ports they hold |
|
|
228
|
+
| `7` | sessions | coding-agent sessions, what they were about, context used |
|
|
229
|
+
| `8` | system | this machine: load, memory, disks, network, exposure |
|
|
230
|
+
| `9` | graph | who started what, where it runs, and what it exposes |
|
|
231
|
+
| `V` | vibe | the ambient screen, for the second monitor |
|
|
232
|
+
|
|
233
|
+
Views 5 and 6 group rather than filter. Every view is a different question asked
|
|
234
|
+
of one scan, not a different scan.
|
|
235
|
+
|
|
236
|
+
## Keys
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
j / k, arrows move tab shift-tab the next view, the
|
|
240
|
+
h / l the dashboard's panes same order as 0-9
|
|
241
|
+
enter, o detail pane / search
|
|
242
|
+
O open it in a browser f a port that is free now, and not
|
|
243
|
+
(ctrl+enter too, where spoken for by anything later
|
|
244
|
+
the terminal sends it) a animation V vibe mode
|
|
245
|
+
0-9 views r rescan now ? keys q quit
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
macOS never delivers Cmd+Enter to a terminal program, so `O` is the binding that
|
|
249
|
+
always works.
|
|
250
|
+
|
|
251
|
+
## The sessions you left open
|
|
252
|
+
|
|
253
|
+
Ten agent windows, none of them closed, and no way to tell which is which.
|
|
254
|
+
|
|
255
|
+
Press **7**. Open sessions first, because those are the only rows you can act
|
|
256
|
+
on; everything under them is a transcript nobody is holding:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
TOOL WHAT IT WAS ABOUT PROJECT CONTEXT VS BIGGEST LAST USED
|
|
260
|
+
4 agent processes running - 30 transcripts on disk
|
|
261
|
+
Claude Code max - default max 20x
|
|
262
|
+
Codex not signed in
|
|
263
|
+
|
|
264
|
+
6 open right now - 2.9M tokens between them - oldest untouched 5h
|
|
265
|
+
* claude Refactor the billing webhook retries payments 215k ██░░░░░░ 2s ago 4 here
|
|
266
|
+
* claude Port the admin table to components admin-ui 904k ████████ 17m ago 4 here
|
|
267
|
+
* claude Trace the flaky integration test api 479k ████░░░░ 26m ago 4 here
|
|
268
|
+
|
|
269
|
+
24 left on disk, no process behind them
|
|
270
|
+
claude Split the worker into two queues worker 935k ████████ 7h ago
|
|
271
|
+
claude Make the search endpoint paginate docs 193k ██░░░░░░ 9h ago
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The bar compares each session with the **biggest one on this machine**, and
|
|
275
|
+
with nothing else. A transcript records the tokens a turn carried; it never
|
|
276
|
+
records the model's context limit, so portlist will not print a percentage of a
|
|
277
|
+
window it cannot read.
|
|
278
|
+
|
|
279
|
+
`enter` opens the rest of it - the first prompt you typed, where the session got
|
|
280
|
+
to, and how to close it:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
context 903,802 tokens on the last turn - 214 turns
|
|
284
|
+
last active 28 Aug 04:14 (17m ago)
|
|
285
|
+
running pid 14502
|
|
286
|
+
close it kill 14502
|
|
287
|
+
first prompt the stripe webhook retries twice on 5xx, work out why and fix...
|
|
288
|
+
last prompt run the migration against staging first
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
When several agents share one directory it says so instead of choosing:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
running pid 9316, 5864, 30000, 43725 - more than one agent is in this
|
|
295
|
+
directory, so which of them is this session cannot be told from outside
|
|
296
|
+
close it from its own window - the line above says why a pid cannot be picked for you
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
A `kill` that might close the wrong window is worse than no command at all.
|
|
300
|
+
|
|
301
|
+
Six tools, each read from the store it already writes:
|
|
302
|
+
|
|
303
|
+
| | |
|
|
304
|
+
|---|---|
|
|
305
|
+
| **Claude Code** | `~/.claude/projects/**/*.jsonl` |
|
|
306
|
+
| **Codex** | `~/.codex/sessions/**/rollout-*.jsonl` |
|
|
307
|
+
| **GitHub Copilot CLI** | `~/.copilot/session-store.db`, which keeps its own summary |
|
|
308
|
+
| **VS Code chat** | `.../Code/User/workspaceStorage/*/chatSessions/*.jsonl` |
|
|
309
|
+
| **Cursor** | the same layout under `Cursor/`, because it is a VS Code fork |
|
|
310
|
+
| **Gemini CLI** | `~/.gemini/tmp/*/logs.json`, where a build writes one |
|
|
311
|
+
|
|
312
|
+
Each gives the generated title, the first prompt you typed, the project, the
|
|
313
|
+
model, the turn count, and where available the **context it is carrying** - the
|
|
314
|
+
token total from the last turn, which is the number that decides what to clear.
|
|
315
|
+
|
|
316
|
+
It also shows which plan each tool is signed in under, so "which account burned
|
|
317
|
+
this week's quota" is answerable:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
Claude Code max - default max 20x
|
|
321
|
+
Codex not signed in
|
|
322
|
+
GitHub Copilot signed in
|
|
323
|
+
Gemini signed in
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Live sessions are matched to running agent processes by working directory. Where
|
|
327
|
+
several agents share one directory it says so rather than guessing.
|
|
328
|
+
|
|
329
|
+
**Prompts never leave the machine.** portlist has no server and no network code
|
|
330
|
+
at all, so there is nowhere for them to go. Account details are narrower still:
|
|
331
|
+
the plan and the organisation, never the address or the account id.
|
|
332
|
+
|
|
333
|
+
It never reads a whole transcript either - they reach eight megabytes. The head
|
|
334
|
+
has the first prompt and the directory, the tail has the title, the latest usage
|
|
335
|
+
and the last activity. Sixty sessions in a tenth of a second.
|
|
336
|
+
|
|
337
|
+
## The graph
|
|
338
|
+
|
|
339
|
+
`9`. Who started what, where it runs and what it exposes, laid out the way a
|
|
340
|
+
terminal draws a layered graph well:
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
STARTED BY PROJECT PROCESS PORT AND SERVICE REACHABLE FROM
|
|
344
|
+
started work in ──▸ runs ──▸ listens ──▸ confirmed on ──▸
|
|
345
|
+
|
|
346
|
+
◆ Claude Code ├─analytics ──Python pid 6810 ──:8000 Python http.serv ──Localhost only
|
|
347
|
+
│ ├─data-export ──node pid 77259 ──:8422 unidentified ──Localhost only
|
|
348
|
+
│ │ ──Python pid 96798──:8787 Python http.serv ──All interfaces confirmed on 192.168.0.2
|
|
349
|
+
│ ├─metrics ──bun pid 32016 ──:48744 Bun ──Localhost only
|
|
350
|
+
│ └─scanner ──Python pid 67222──:8787 FastAPI / Uvicorn──Localhost only
|
|
351
|
+
◇ launchd └─no project ──tor pid 58508 ──:9050 Tor ──Localhost only
|
|
352
|
+
|
|
353
|
+
◆ an agent session ◇ something else 10 of 12 services were started by an agent
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
One line is one service, and a parent is printed once and carried down with a
|
|
357
|
+
rule, which is what makes the sharing visible: eleven services under one agent
|
|
358
|
+
session, four in one project. The edge names are the ones the web version uses,
|
|
359
|
+
so both surfaces describe the machine with one vocabulary.
|
|
360
|
+
|
|
361
|
+
Narrow terminals get the same graph as headed groups, because that is what a
|
|
362
|
+
tree looks like when it runs out of width.
|
|
363
|
+
|
|
364
|
+
## Vibe mode
|
|
365
|
+
|
|
366
|
+
Press **V**, or leave it alone for thirty seconds:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
P O R T L I S T
|
|
370
|
+
|
|
371
|
+
LOCAL NETWORK
|
|
372
|
+
|
|
373
|
+
○ :8787
|
|
374
|
+
:8422 ○ · · · ○ :8807
|
|
375
|
+
····· · ·····
|
|
376
|
+
:8078 ○ ···················HOST···········●······ ◉ :7337
|
|
377
|
+
····· · ·····
|
|
378
|
+
:9050 ○ · · · ○ :8000
|
|
379
|
+
|
|
380
|
+
1 connection observed between local services
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Seven scenes rotate: the cockpit (everything at once), a grid of every listening
|
|
384
|
+
service, the machine and its
|
|
385
|
+
meters, the network between local services, each agent and what it started,
|
|
386
|
+
what has actually happened lately, and **the room** - a plate that ships with
|
|
387
|
+
the program, drawn as characters, with the three numbers that fit placed where
|
|
388
|
+
it is dark. Five themes, four speeds, `t` and `s` to
|
|
389
|
+
cycle them, any other key to come back.
|
|
390
|
+
|
|
391
|
+
The room scene ships a picture so that it is a scene at all, but that is a
|
|
392
|
+
default and not a fixture: set a picture of your own and the room shows yours.
|
|
393
|
+
A picture of yours decorates the other six: name it on the way in with
|
|
394
|
+
`portlist --vibe-bg thing.png`, or drop PNGs into `~/.portlist/backgrounds/` and
|
|
395
|
+
press `g` inside vibe mode to walk them, with the footer naming what is showing.
|
|
396
|
+
|
|
397
|
+
`g` covers both which picture and how it is drawn:
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
none -> room.png as an image -> room.png as characters -> none
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**As an image** means the terminal is handed the PNG and draws it itself, behind
|
|
404
|
+
the text. **As characters** is the density rendering: `b` sets how strongly it
|
|
405
|
+
shows, 0 to 100, and `B` decides which end of the picture becomes ink.
|
|
406
|
+
|
|
407
|
+
### Which terminal shows the real picture
|
|
408
|
+
|
|
409
|
+
Almost no terminal can draw a picture. The ones that can speak **kitty's
|
|
410
|
+
graphics protocol**, and portlist uses only that protocol, for one reason: it
|
|
411
|
+
has a z-index, and `z=-1` is the only arrangement that puts a picture *behind*
|
|
412
|
+
the readings. iTerm2's inline images and sixel both occupy cells, so a picture
|
|
413
|
+
drawn either way would cover the numbers, and a screen whose numbers are hidden
|
|
414
|
+
by decoration is worse than a screen with no decoration. So iTerm2 and every
|
|
415
|
+
sixel terminal get the character rendering too.
|
|
416
|
+
|
|
417
|
+
If the ring goes straight from `none` to *as characters*, your terminal cannot
|
|
418
|
+
do it. That is not a setting you are missing.
|
|
419
|
+
|
|
420
|
+
**macOS** - Terminal.app cannot, and no preference changes that. iTerm2 cannot
|
|
421
|
+
either, for the reason above. Install one of:
|
|
422
|
+
|
|
423
|
+
```sh
|
|
424
|
+
brew install --cask ghostty # verified working, and needs no configuration
|
|
425
|
+
brew install --cask kitty # the reference implementation of the protocol
|
|
426
|
+
brew install --cask wezterm
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
If Ghostty is the first thing you install, note that its `TERM` is
|
|
430
|
+
`xterm-ghostty`, which is not in the terminfo database most systems ship. Recent
|
|
431
|
+
versions install it for you; if portlist will not start there at all, that is
|
|
432
|
+
why, and this fixes it:
|
|
433
|
+
|
|
434
|
+
```sh
|
|
435
|
+
mkdir -p ~/.terminfo/78
|
|
436
|
+
cp /Applications/Ghostty.app/Contents/Resources/terminfo/78/xterm-ghostty ~/.terminfo/78/
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Linux** - several work. kitty and Ghostty are packaged for most distributions;
|
|
440
|
+
Konsole (KDE) and WezTerm implement the protocol as well.
|
|
441
|
+
|
|
442
|
+
```sh
|
|
443
|
+
sudo apt install kitty # Debian, Ubuntu
|
|
444
|
+
sudo dnf install kitty # Fedora
|
|
445
|
+
sudo pacman -S kitty # Arch
|
|
446
|
+
curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Windows** - Windows Terminal speaks sixel, not this protocol, so it draws
|
|
450
|
+
characters. **WezTerm** is the one that works:
|
|
451
|
+
|
|
452
|
+
```powershell
|
|
453
|
+
winget install wez.wezterm
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Under WSL the terminal is whatever is drawing the window, so the same rule
|
|
457
|
+
applies: WezTerm yes, Windows Terminal characters.
|
|
458
|
+
|
|
459
|
+
**Over ssh** it works when the terminal in front of *you* supports it, because
|
|
460
|
+
the escape travels down the connection like any other output. **Inside tmux** it
|
|
461
|
+
usually does not, unless tmux is configured to pass the escape through. The
|
|
462
|
+
character rendering has none of these conditions, which is why it stays the
|
|
463
|
+
default answer rather than a fallback.
|
|
464
|
+
|
|
465
|
+
Nothing is written to a terminal that has not said it speaks the protocol, so
|
|
466
|
+
being wrong about support costs a blank background rather than escape codes
|
|
467
|
+
across your screen. To force characters everywhere, including on a terminal that
|
|
468
|
+
could show the file, set `PORTLIST_NO_GRAPHICS=1` or press `g` one more step.
|
|
469
|
+
|
|
470
|
+
**Confirmed on Ghostty**, macOS, with the picture behind the readings as
|
|
471
|
+
intended. kitty is the reference implementation of the protocol and Konsole and
|
|
472
|
+
WezTerm implement it too, but I have not put eyes on those three, and terminals
|
|
473
|
+
differ in how completely they follow the spec. If yours honours the protocol
|
|
474
|
+
without the z-index the picture will sit *over* the text rather than behind it:
|
|
475
|
+
press `g` once more for characters, and open an issue naming the terminal.
|
|
476
|
+
|
|
477
|
+
When a service really appears while you are watching, it is marked **NEW** for a
|
|
478
|
+
few seconds and the strip redraws around it; when one stops listening, that is
|
|
479
|
+
reported too. The first frame marks nothing, because everything is new to the
|
|
480
|
+
screen the moment it opens and none of it is new to the machine.
|
|
481
|
+
|
|
482
|
+
**Nothing on that screen moves unless something was measured.** A dot pulses
|
|
483
|
+
because that service was measured busy inside the last minute. A particle
|
|
484
|
+
crosses an edge because a loopback connection between those two ports was
|
|
485
|
+
observed. Where nothing has been measured, it says so and sits still: inventing
|
|
486
|
+
motion would make the prettiest part of the program the one lying to you.
|
|
487
|
+
|
|
488
|
+
## What it can tell you that `lsof` cannot
|
|
489
|
+
|
|
490
|
+
- **Who started it.** Claude Code, Cursor, Codex, Copilot, Aider, Goose,
|
|
491
|
+
Windsurf, a terminal, a service manager - from process ancestry first and the
|
|
492
|
+
environment second. And whether that session has since exited.
|
|
493
|
+
- **Whether it survived a restart.** A launch record is written the first time a
|
|
494
|
+
service is seen and never rewritten, so attribution outlives both the agent
|
|
495
|
+
exiting and the service being restarted by something else.
|
|
496
|
+
- **Whether anyone is using it.** Measured over time, not inferred from uptime.
|
|
497
|
+
- **Whether the network can reach it**, checked by connecting to this machine's
|
|
498
|
+
real address rather than reading a bind string.
|
|
499
|
+
- **Which container holds the port**, and which compose project it belongs to.
|
|
500
|
+
- **A port that is free** now and not spoken for by anything you run later.
|
|
501
|
+
|
|
502
|
+
## It never stops anything
|
|
503
|
+
|
|
504
|
+
The detail pane prints the command; you run it. There is no kill key, no daemon,
|
|
505
|
+
and nothing here writes to another machine.
|
|
506
|
+
|
|
507
|
+
It does open sockets, and it is worth being exact about which. It connects
|
|
508
|
+
*outward* to the ports on this machine to see what answers, and it binds a
|
|
509
|
+
candidate port for a moment to check it is free, then closes it. Neither ever
|
|
510
|
+
calls `listen()`, so portlist has no port of its own and nothing can connect
|
|
511
|
+
to it.
|
|
512
|
+
|
|
513
|
+
## Where its data lives
|
|
514
|
+
|
|
515
|
+
`~/.portlist/` - the launch ledger, use history and the recipe book. Override
|
|
516
|
+
with `--data-dir` or `PORTLIST_DATA`.
|
|
517
|
+
|
|
518
|
+
## Requirements
|
|
519
|
+
|
|
520
|
+
Python 3.9+ with `curses`, standard on macOS, Linux and BSD. On Windows,
|
|
521
|
+
`pip install windows-curses`, which `pipx install portlist` does for you. No
|
|
522
|
+
third-party packages on any platform otherwise, ever.
|
|
523
|
+
|
|
524
|
+
## Documentation
|
|
525
|
+
|
|
526
|
+
- [Setup](docs/SETUP.md) - every install route, and what each one puts where
|
|
527
|
+
- [Usage](docs/USAGE.md) - the views, the keys, and what each column means
|
|
528
|
+
- [Features](docs/FEATURES.md) - the full list, and how each answer is reached
|
|
529
|
+
- [Architecture](docs/ARCHITECTURE.md) - one scan, one model, ten views
|
|
530
|
+
- [Motion](docs/MOTION.md) - the animation language, and why each thing moves
|
|
531
|
+
- [Security](SECURITY.md) - what it reads, what it never sends
|
|
532
|
+
- [Contributing](CONTRIBUTING.md)
|
|
533
|
+
|
|
534
|
+
MIT.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
If portlist saved you an afternoon of `lsof | grep`, you can
|
|
539
|
+
[buy me a coffee](https://buymeacoffee.com/mr.hunt.007). Entirely optional: the
|
|
540
|
+
tool is free, has no telemetry, and will stay that way.
|