4top 0.2.0a1__py3-none-any.whl
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.
- 4top-0.2.0a1.dist-info/METADATA +233 -0
- 4top-0.2.0a1.dist-info/RECORD +18 -0
- 4top-0.2.0a1.dist-info/WHEEL +5 -0
- 4top-0.2.0a1.dist-info/entry_points.txt +2 -0
- 4top-0.2.0a1.dist-info/licenses/LICENSE +21 -0
- 4top-0.2.0a1.dist-info/top_level.txt +1 -0
- fourtop/__init__.py +3 -0
- fourtop/__main__.py +3 -0
- fourtop/agents.py +150 -0
- fourtop/app.py +824 -0
- fourtop/cli.py +251 -0
- fourtop/config.py +167 -0
- fourtop/doctor.py +90 -0
- fourtop/errors.py +27 -0
- fourtop/hosts.py +167 -0
- fourtop/models.py +76 -0
- fourtop/services.py +269 -0
- fourtop/state.py +53 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 4top
|
|
3
|
+
Version: 0.2.0a1
|
|
4
|
+
Summary: Your coding agents, one terminal. Find, search and resume native sessions, locally or over SSH.
|
|
5
|
+
Author: 4ier
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/4ier/4top
|
|
8
|
+
Project-URL: Issues, https://github.com/4ier/4top/issues
|
|
9
|
+
Keywords: coding-agents,terminal,session-history,claude,codex,pi,ssh
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console :: Curses
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: session-ls<0.3,>=0.2.0
|
|
19
|
+
Requires-Dist: textual<9,>=8.2.8
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio<2,>=0.23; extra == "dev"
|
|
23
|
+
Requires-Dist: pexpect<5,>=4.9; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
|
|
25
|
+
Requires-Dist: build<2,>=1; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# 4top
|
|
29
|
+
|
|
30
|
+
**Your coding agents, one terminal.**
|
|
31
|
+
|
|
32
|
+
Find your work. Resume an exact native session. Reach your other machines over SSH.
|
|
33
|
+
A keyboard-first terminal dashboard built on **session-ls**, with no extra 4top
|
|
34
|
+
daemon, account, model calls, or telemetry.
|
|
35
|
+
|
|
36
|
+
[中文](README.zh-CN.md) · [Compatibility](docs/compatibility.md) · [Remote hosts](docs/remote-design.md) · [Acceptance](docs/validation/README.md)
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
> **0.2.0a1 — alpha.** The current development line owns no process and drives no
|
|
41
|
+
> multiplexer of its own. Codex **0.155.1** passed an authenticated exact-resume
|
|
42
|
+
> smoke check against an earlier build;
|
|
43
|
+
> Claude Code, Pi and other native versions are not certified.
|
|
44
|
+
> [Evidence](docs/validation/macos-0.1.0a2.md). This release is not on PyPI.
|
|
45
|
+
|
|
46
|
+
## Install from this checkout
|
|
47
|
+
|
|
48
|
+
macOS or Linux; **Python 3.11+**. No multiplexer is required. Current validation
|
|
49
|
+
records, rather than this minimum target, determine which versions were tested.
|
|
50
|
+
Windows users need a Linux environment such as WSL; native Windows is unsupported.
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git clone https://github.com/4ier/4top.git
|
|
54
|
+
cd 4top
|
|
55
|
+
python3 -m venv .venv
|
|
56
|
+
.venv/bin/python -m pip install ./packages/session-ls .
|
|
57
|
+
.venv/bin/4top --demo
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Both local packages are intentionally supplied to pip: `session-ls 0.2.0` is not
|
|
61
|
+
assumed to exist on a package registry. The root wheel contains only `fourtop`;
|
|
62
|
+
the independent `session-ls` package retains its small, stdlib-only CLI.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
. .venv/bin/activate
|
|
66
|
+
4top # browse every session on this machine
|
|
67
|
+
4top new codex # start an agent here, in this terminal
|
|
68
|
+
4top --host build-box # view another machine over ssh
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
4top never installs or authenticates agents for you. Install the original Claude
|
|
72
|
+
Code, Codex, or Pi CLI separately and keep its existing authentication flow.
|
|
73
|
+
The panel also reads Cursor transcripts, but does not launch or resume Cursor.
|
|
74
|
+
|
|
75
|
+
## The daily loop
|
|
76
|
+
|
|
77
|
+
Open `4top` and you get every session on the machine, most recent first. Select a
|
|
78
|
+
row and press **Enter**: 4top asks for confirmation and then runs the native CLI
|
|
79
|
+
in this terminal to resume that exact session. Leave the agent and you are back in
|
|
80
|
+
the panel. **`q` closes only the panel.**
|
|
81
|
+
|
|
82
|
+
A plain terminal or any multiplexer you already run is equally fine: 4top starts
|
|
83
|
+
the agent in the terminal it was launched from and never allocates a terminal of
|
|
84
|
+
its own. If you want a session to survive closing your laptop, run 4top inside the
|
|
85
|
+
multiplexer you already use.
|
|
86
|
+
|
|
87
|
+
| Key | Action |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `↑` / `↓`, `Enter` | Select and resume |
|
|
90
|
+
| `H` | Switch the panel between this machine and a configured host |
|
|
91
|
+
| `/`, `Enter`, `Esc` | Search metadata, return to table, clear/cancel |
|
|
92
|
+
| `Ctrl-F` | Explicit literal full-content search; `Esc` cancels |
|
|
93
|
+
| `Space`, `i` | Read-only preview, details |
|
|
94
|
+
| `n`, `r`, `?` | New agent, refresh, help |
|
|
95
|
+
| `q`, `Ctrl-C` | Close only the panel |
|
|
96
|
+
|
|
97
|
+
Search supports case-insensitive words and quoted phrases; all terms must match.
|
|
98
|
+
Full search decodes JSON text, including Chinese escaped as `\u....`. It reads
|
|
99
|
+
only configured sources, reports partial scans, and never executes transcript
|
|
100
|
+
content. The UI renders titles and previews as plain, sanitized text.
|
|
101
|
+
|
|
102
|
+
## Sessions, not processes
|
|
103
|
+
|
|
104
|
+
A native agent is a file. `pi --session <path>`, `claude --resume <id>` and
|
|
105
|
+
`codex resume <id>` all work from the transcript alone, so the transcript is the
|
|
106
|
+
thing 4top tracks, and the process is the ephemeral part.
|
|
107
|
+
|
|
108
|
+
**Resume** starts a new native process from an exact ID or source path. It cannot
|
|
109
|
+
restore lost memory, network connections, shell children, or a destroyed machine.
|
|
110
|
+
Resume uses the CLI's **current native configuration**; 4top does not replay the
|
|
111
|
+
original launch flags. Review native permissions before sending another task.
|
|
112
|
+
|
|
113
|
+
Because 4top owns no process it claims nothing about liveness either: a row is a
|
|
114
|
+
session you can resume, and that is all it says. Failed queries are reported as
|
|
115
|
+
issues and never rendered as an empty machine.
|
|
116
|
+
|
|
117
|
+
Two consequences worth knowing. A resumed agent is a **new** process; two agents in
|
|
118
|
+
one directory still have **no code/worktree isolation**. And `4top new` runs the
|
|
119
|
+
agent in the foreground of the terminal you launched it from: outside a multiplexer
|
|
120
|
+
it ends with that terminal.
|
|
121
|
+
|
|
122
|
+
## Remote hosts over SSH
|
|
123
|
+
|
|
124
|
+
Point 4top at any machine you can already `ssh` into. There is no daemon to
|
|
125
|
+
install, no port to open, and no credential store: the remote side is the same
|
|
126
|
+
CLI, and the local 4top only runs it.
|
|
127
|
+
|
|
128
|
+
```toml
|
|
129
|
+
# ~/.config/4top/config.toml
|
|
130
|
+
[hosts.build-box]
|
|
131
|
+
ssh = "me@build-box" # any ssh destination, including a tailnet name
|
|
132
|
+
# command = "/opt/4top/bin/4top" # if a non-login PATH does not include 4top
|
|
133
|
+
# refresh_seconds = 15.0 # slower than local: each tick is a round trip
|
|
134
|
+
# timeout_seconds = 10.0
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
4top --host build-box # the whole panel, scoped to that host
|
|
139
|
+
4top --host build-box list --json
|
|
140
|
+
4top --host me@10.0.0.4 doctor # an unconfigured target works too
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Views stay isolated: the default scope is this machine, and a host replaces it
|
|
144
|
+
rather than merging machines into one table. `H` switches the panel between them
|
|
145
|
+
without leaving it; `--host NAME` starts the panel already scoped. Anything that starts a process runs
|
|
146
|
+
**on that host** through `ssh -t`, so the resumed agent lives where its history
|
|
147
|
+
lives; the remote CLI does the work and the local side only hands over the
|
|
148
|
+
terminal. Connection reuse (`ControlMaster`) keeps refreshes cheap, `BatchMode`
|
|
149
|
+
means a missing key fails fast instead of prompting, and a remote that speaks a
|
|
150
|
+
different row schema is refused instead of partially parsed.
|
|
151
|
+
|
|
152
|
+
Before it hands over the terminal, the panel asks the host that owns the session
|
|
153
|
+
whether the resume can work there (`4top check`). A host without that agent
|
|
154
|
+
installed, or a session whose directory is gone, is reported in the panel instead
|
|
155
|
+
of failing during the hand-over, where the message would be painted over. The ssh
|
|
156
|
+
connection also keeps a liveness probe, so a link that dies becomes an error rather
|
|
157
|
+
than a hang, and the session stays in its transcript to be resumed again.
|
|
158
|
+
|
|
159
|
+
## Command line
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
4top list --json # one JSON object per row
|
|
163
|
+
4top list --agent pi --project 4top
|
|
164
|
+
4top search 'retry "database timeout"' # metadata match
|
|
165
|
+
4top search '中文' --full # decoded full-content search
|
|
166
|
+
4top preview h_<key> # one bounded read-only page
|
|
167
|
+
4top check h_<key> --json # would a resume work here, and why not
|
|
168
|
+
4top new codex -- --model MODEL # native arguments after --
|
|
169
|
+
4top resume h_<key> --yes # restore this process as the agent
|
|
170
|
+
4top doctor --json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`doctor` also reports `revision`, and `4top --host NAME doctor` reports it for both
|
|
174
|
+
sides, so a remote running older code is visible instead of failing later. Bring a
|
|
175
|
+
remote forward with `scripts/remote_update.py NAME`; it uses the host's own egress
|
|
176
|
+
first and falls back to a tunnel from this machine.
|
|
177
|
+
|
|
178
|
+
`--config`, `--host` and `--no-color` work before or after the subcommand.
|
|
179
|
+
`check` exits 0 when the session can resume here and 3 when it cannot. Keys
|
|
180
|
+
may be shortened only when their prefixes are unambiguous (at least four
|
|
181
|
+
characters). Row numbers are never execution targets. `list --json` rows carry
|
|
182
|
+
`schema_version`, `key`, `agent`, `host`, `cwd`, `title`, `started`, `last`,
|
|
183
|
+
`source`, `status` and `can_resume`.
|
|
184
|
+
|
|
185
|
+
## Configuration and privacy
|
|
186
|
+
|
|
187
|
+
Optional configuration: `$XDG_CONFIG_HOME/4top/config.toml` (default
|
|
188
|
+
`~/.config/4top/config.toml`). No setup file is needed for standard stores.
|
|
189
|
+
|
|
190
|
+
```toml
|
|
191
|
+
[ui]
|
|
192
|
+
refresh_seconds = 1.0
|
|
193
|
+
history_refresh_seconds = 5.0
|
|
194
|
+
color = "auto" # or "none"; NO_COLOR is also supported
|
|
195
|
+
|
|
196
|
+
[history]
|
|
197
|
+
metadata_max_bytes = 2097152
|
|
198
|
+
metadata_max_lines = 2000
|
|
199
|
+
preview_max_lines = 200
|
|
200
|
+
|
|
201
|
+
[agents.codex]
|
|
202
|
+
# root = "/absolute/path/to/codex-home"
|
|
203
|
+
# executable = "/absolute/path/to/a-real-wrapper"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Agent store roots respect `CODEX_HOME`, `CLAUDE_CONFIG_DIR`, and
|
|
207
|
+
`PI_CODING_AGENT_DIR`; an explicit configured root wins. Selected history and
|
|
208
|
+
launch profile must agree. Local state is private: `$XDG_STATE_HOME/4top` holds a
|
|
209
|
+
local identity and your last selection, and `$XDG_CACHE_HOME/4top` holds
|
|
210
|
+
rebuildable metadata. No environment values, prompt text, or transcripts are
|
|
211
|
+
retained. The only network access is the ssh you configured.
|
|
212
|
+
|
|
213
|
+
[Privacy](docs/privacy.md) · [Troubleshooting](docs/troubleshooting.md) · [Design](docs/design.md)
|
|
214
|
+
|
|
215
|
+
## Develop and contribute
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
.venv/bin/python -m pip install -e ./packages/session-ls -e '.[dev]'
|
|
219
|
+
.venv/bin/python -m pytest
|
|
220
|
+
.venv/bin/python -m ruff check .
|
|
221
|
+
uv tool install --force --editable . # optional: `4top` runs this checkout
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Tests use private temporary HOME/state directories and synthetic agents; the ssh
|
|
225
|
+
tests use a fake `ssh` on `PATH`. No account credentials, network access or model
|
|
226
|
+
calls are required. Record your OS, Python and native CLI versions when reporting
|
|
227
|
+
compatibility. **Never post raw transcripts or tokens.**
|
|
228
|
+
|
|
229
|
+
For fresh installation, CI, reproducible demo export, native smoke testing, and
|
|
230
|
+
release gates, see [CONTRIBUTING](CONTRIBUTING.md) and the [acceptance guide](docs/validation/README.md).
|
|
231
|
+
|
|
232
|
+
4top builds on 4ier's `session-ls` parsers and uses Textual. It is not affiliated
|
|
233
|
+
with the vendors of the supported coding agents. **MIT licensed.**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
4top-0.2.0a1.dist-info/licenses/LICENSE,sha256=cYZ3LVkc_-w5u7NQqO4QjWZ6Z9daosrMOOrb5IOXdeI,1061
|
|
2
|
+
fourtop/__init__.py,sha256=FRTmk3XpzLxsNjuDlaL88OncrVEG6MFy1nVQh0sCSPw,84
|
|
3
|
+
fourtop/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
4
|
+
fourtop/agents.py,sha256=hieUJ-LpIQR3Mmy-GqYuwhw9WqVqFyAUy7N_zZ4no3Y,7302
|
|
5
|
+
fourtop/app.py,sha256=6kSrPjwhidox7rwipOkzWFY9V_iRkkGL7Y1Dd1-yJnE,35575
|
|
6
|
+
fourtop/cli.py,sha256=VtbihQvWxY4tdl-LS2aD2Jf2Tf6qxwul0oPgV_245u8,11798
|
|
7
|
+
fourtop/config.py,sha256=VebU1P-y0cIHC13PNiReM0GK02nHEa-TTJzqY4fpwGw,8190
|
|
8
|
+
fourtop/doctor.py,sha256=g3fuBcwT3UVMJ-FMXt3Ci4tH3a9FxcyEQaneW_UlIyc,4341
|
|
9
|
+
fourtop/errors.py,sha256=u10_hiP9bNlfoBRAafumcWCH8uVs512MTSabof3TNsg,642
|
|
10
|
+
fourtop/hosts.py,sha256=RyIXxdn6aA2kC7oCT6jsMx6MJXTM459N0ngUdTV7oX8,7525
|
|
11
|
+
fourtop/models.py,sha256=AJCs3GkHmwEqoIY7jKzBsCAgki-RCKhVqLJZKXAfsnE,2094
|
|
12
|
+
fourtop/services.py,sha256=_90mGp_a_ofuLPg64EK7ozSOi-Fo2iyQ-CaTAkGarbo,11686
|
|
13
|
+
fourtop/state.py,sha256=SKC3_U53feXhZGItOiTVMEyHJc74HJUe0MxjOsdopDw,2033
|
|
14
|
+
4top-0.2.0a1.dist-info/METADATA,sha256=sYv642XMA75RNwb5ejDkmESQI0pl3-K1AoYjsT2mBHY,10475
|
|
15
|
+
4top-0.2.0a1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
4top-0.2.0a1.dist-info/entry_points.txt,sha256=TREhJAejvlOwHehlUj6meVh7JQjWrwuKASdTKp1I3NA,42
|
|
17
|
+
4top-0.2.0a1.dist-info/top_level.txt,sha256=-A1z8S-VOyAvinHumQEZKDjfomX0kl7H2LxzibJulPg,8
|
|
18
|
+
4top-0.2.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 4ier
|
|
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 @@
|
|
|
1
|
+
fourtop
|
fourtop/__init__.py
ADDED
fourtop/__main__.py
ADDED
fourtop/agents.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Native CLI plans. Capability probes do not authenticate or call a model."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from session_ls.api import HistoryRecord, history_key, is_uuid, open_source
|
|
12
|
+
|
|
13
|
+
from .config import Config
|
|
14
|
+
from .errors import Dependency, FourtopError, Missing
|
|
15
|
+
from .models import LaunchPlan
|
|
16
|
+
|
|
17
|
+
RESERVED = {
|
|
18
|
+
"claude": {"--resume", "-r", "--continue", "-c", "--session-id", "--fork-session",
|
|
19
|
+
"--no-session-persistence", "--print", "-p", "--input-format", "--output-format"},
|
|
20
|
+
"codex": {"--cd", "-C", "--ephemeral", "--resume", "--session-id"},
|
|
21
|
+
"pi": {"--resume", "-r", "--continue", "-c", "--session", "--session-id", "--session-dir",
|
|
22
|
+
"--no-session", "--print", "-p", "--mode"},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Capability probes are local `--help` / `--version` calls; they never authenticate.
|
|
26
|
+
PROBE_SECONDS = 5.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Capability:
|
|
31
|
+
executable: str
|
|
32
|
+
help_ok: bool
|
|
33
|
+
allocate_id: bool
|
|
34
|
+
resume: bool
|
|
35
|
+
version: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_extra(agent: str, extra: tuple[str, ...]) -> None:
|
|
39
|
+
for item in extra:
|
|
40
|
+
if "\x00" in item:
|
|
41
|
+
raise FourtopError("NUL is not allowed in command arguments", 2)
|
|
42
|
+
flag = item.split("=", 1)[0]
|
|
43
|
+
if flag in RESERVED[agent]:
|
|
44
|
+
raise FourtopError(f"{flag} changes runtime identity or mode; use a dedicated 4top command", 2)
|
|
45
|
+
# Native parsers accept compact short options such as -rUUID and -C/path.
|
|
46
|
+
if len(item) > 2 and not item.startswith("--"):
|
|
47
|
+
if item[:2] in RESERVED[agent]:
|
|
48
|
+
raise FourtopError(f"{item[:2]} conflicts with 4top's launch plan", 2)
|
|
49
|
+
# CLI subcommands are allowed after global flags by native parsers. Reject them
|
|
50
|
+
# anywhere, rather than letting --model X resume silently escape new semantics.
|
|
51
|
+
if agent == "codex" and any(item in {"resume", "fork", "exec", "e", "app-server", "mcp-server",
|
|
52
|
+
"login", "logout", "mcp", "app", "sandbox", "debug",
|
|
53
|
+
"apply", "cloud", "features", "completion", "help"}
|
|
54
|
+
for item in extra):
|
|
55
|
+
raise FourtopError("Use the native interactive command; subcommands are not accepted by new", 2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Drivers:
|
|
59
|
+
def __init__(self, config: Config, host_id: str):
|
|
60
|
+
self.config = config
|
|
61
|
+
self.host_id = host_id
|
|
62
|
+
self._probes = {}
|
|
63
|
+
|
|
64
|
+
def executable(self, agent: str) -> str:
|
|
65
|
+
if agent not in RESERVED:
|
|
66
|
+
raise Dependency(f"{agent} is read-only or unsupported for runtime operations")
|
|
67
|
+
configured = self.config.executables[agent]
|
|
68
|
+
found = shutil.which(configured, path=self.config.environment.get("PATH", os.defpath))
|
|
69
|
+
if not found:
|
|
70
|
+
raise Dependency(f"{agent} executable not found; install it or configure a real wrapper path")
|
|
71
|
+
return os.path.abspath(found)
|
|
72
|
+
|
|
73
|
+
def probe(self, agent: str) -> Capability:
|
|
74
|
+
executable = self.executable(agent)
|
|
75
|
+
st = os.stat(executable)
|
|
76
|
+
key = (executable, st.st_mtime_ns, st.st_size)
|
|
77
|
+
if key in self._probes:
|
|
78
|
+
return self._probes[key]
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run([executable, "--help"], capture_output=True, text=True,
|
|
81
|
+
errors="replace", timeout=PROBE_SECONDS,
|
|
82
|
+
env=self.config.environment, cwd="/")
|
|
83
|
+
help_text = result.stdout + result.stderr
|
|
84
|
+
ok = result.returncode == 0
|
|
85
|
+
version_result = subprocess.run([executable, "--version"], capture_output=True, text=True,
|
|
86
|
+
errors="replace", timeout=PROBE_SECONDS,
|
|
87
|
+
env=self.config.environment, cwd="/")
|
|
88
|
+
version = version_result.stdout.strip()[:160] if version_result.returncode == 0 else "unknown"
|
|
89
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
90
|
+
ok, help_text, version = False, "", "unavailable"
|
|
91
|
+
resume_token = "resume" if agent == "codex" else "--resume" if agent == "claude" else "--session"
|
|
92
|
+
value = Capability(executable, ok, ok and agent != "codex" and "--session-id" in help_text,
|
|
93
|
+
ok and resume_token in help_text, version)
|
|
94
|
+
self._probes[key] = value
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def cwd(value: str) -> str:
|
|
99
|
+
if not value or "\x00" in value:
|
|
100
|
+
raise Missing("A working directory is required")
|
|
101
|
+
path = Path(value).expanduser()
|
|
102
|
+
if not path.is_absolute():
|
|
103
|
+
path = Path.cwd() / path
|
|
104
|
+
if not path.is_dir() or not os.access(path, os.R_OK | os.X_OK):
|
|
105
|
+
raise Missing(f"Directory does not exist or is not accessible: {path}")
|
|
106
|
+
return str(path.resolve())
|
|
107
|
+
|
|
108
|
+
def plan_new(self, agent: str, cwd: str, extra: tuple[str, ...] = ()) -> LaunchPlan:
|
|
109
|
+
executable = self.executable(agent)
|
|
110
|
+
validate_extra(agent, extra)
|
|
111
|
+
cwd = self.cwd(cwd)
|
|
112
|
+
capability = self.probe(agent)
|
|
113
|
+
native_id = str(uuid.uuid4()) if capability.allocate_id else None
|
|
114
|
+
root = self.config.root(agent).path
|
|
115
|
+
args = [executable]
|
|
116
|
+
if native_id:
|
|
117
|
+
args.extend(["--session-id", native_id])
|
|
118
|
+
args.extend(extra)
|
|
119
|
+
env = dict(self.config.environment)
|
|
120
|
+
key = history_key(self.host_id, agent, root, native_id) if native_id else None
|
|
121
|
+
return LaunchPlan(agent, executable, tuple(args), cwd, env, root, native_id, key)
|
|
122
|
+
|
|
123
|
+
def plan_resume(self, record: HistoryRecord, cwd: str | None = None) -> LaunchPlan:
|
|
124
|
+
if record.agent not in RESERVED:
|
|
125
|
+
raise Dependency("This history adapter is read-only; native resume is not supported")
|
|
126
|
+
if record.root != self.config.root(record.agent).path:
|
|
127
|
+
raise Dependency("History root differs from the selected agent profile")
|
|
128
|
+
if not cwd and record.cwd_quality != "native":
|
|
129
|
+
raise Missing("Historical directory is inferred or missing; provide --cwd explicitly")
|
|
130
|
+
cwd = self.cwd(cwd or record.cwd)
|
|
131
|
+
# Reopen the exact approved source immediately before planning an executable action.
|
|
132
|
+
try:
|
|
133
|
+
with open_source(record.file, record.root):
|
|
134
|
+
pass
|
|
135
|
+
except (OSError, ValueError):
|
|
136
|
+
raise Missing("The selected history file is missing or not trusted") from None
|
|
137
|
+
capability = self.probe(record.agent)
|
|
138
|
+
if not capability.resume:
|
|
139
|
+
raise Dependency("Installed CLI did not advertise a supported resume interface (--help)")
|
|
140
|
+
if record.agent in ("claude", "codex") and not is_uuid(record.native_id):
|
|
141
|
+
raise Dependency("An exact UUID is required; 4top will not guess the latest session")
|
|
142
|
+
if record.agent == "claude":
|
|
143
|
+
args = (capability.executable, "--resume", record.native_id)
|
|
144
|
+
elif record.agent == "codex":
|
|
145
|
+
args = (capability.executable, "resume", record.native_id)
|
|
146
|
+
else:
|
|
147
|
+
args = (capability.executable, "--session", record.file)
|
|
148
|
+
env = dict(self.config.environment)
|
|
149
|
+
return LaunchPlan(record.agent, capability.executable, args, cwd, env, record.root,
|
|
150
|
+
record.native_id, record.key)
|