agents-fleet 0.5.0__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.
- agents_fleet-0.5.0.dist-info/METADATA +232 -0
- agents_fleet-0.5.0.dist-info/RECORD +59 -0
- agents_fleet-0.5.0.dist-info/WHEEL +4 -0
- agents_fleet-0.5.0.dist-info/entry_points.txt +3 -0
- agents_fleet-0.5.0.dist-info/licenses/LICENSE +21 -0
- fleet/__init__.py +0 -0
- fleet/__main__.py +15 -0
- fleet/agents/__init__.py +36 -0
- fleet/agents/clients.py +165 -0
- fleet/agents/docs.py +272 -0
- fleet/agents/registry.py +341 -0
- fleet/agents/usage.py +247 -0
- fleet/cli.py +2216 -0
- fleet/config.py +94 -0
- fleet/edit.py +155 -0
- fleet/install.py +400 -0
- fleet/links.py +26 -0
- fleet/mcpserver.py +282 -0
- fleet/models.py +219 -0
- fleet/onboard.py +174 -0
- fleet/ops/__init__.py +19 -0
- fleet/ops/enrol.py +211 -0
- fleet/ops/errors.py +22 -0
- fleet/ops/firstrun.py +129 -0
- fleet/ops/handover.py +369 -0
- fleet/ops/identity.py +63 -0
- fleet/ops/join.py +476 -0
- fleet/ops/lifecycle.py +186 -0
- fleet/ops/member.py +144 -0
- fleet/ops/migrate.py +79 -0
- fleet/ops/names.py +18 -0
- fleet/ops/rows.py +119 -0
- fleet/ops/sweep.py +411 -0
- fleet/ops/sync.py +301 -0
- fleet/probe/__init__.py +0 -0
- fleet/probe/parse.py +237 -0
- fleet/probe/payload.ps1 +86 -0
- fleet/probe/payload.sh +257 -0
- fleet/probe/runner.py +383 -0
- fleet/reconcile.py +232 -0
- fleet/render/__init__.py +11 -0
- fleet/render/staleness.py +32 -0
- fleet/render/top.py +407 -0
- fleet/render/view.py +449 -0
- fleet/secrets.py +60 -0
- fleet/serve.py +212 -0
- fleet/service.py +437 -0
- fleet/ssh/__init__.py +15 -0
- fleet/ssh/auth.py +74 -0
- fleet/ssh/authkeys.py +199 -0
- fleet/ssh/cmd.py +421 -0
- fleet/ssh/keys.py +342 -0
- fleet/state/__init__.py +17 -0
- fleet/state/access.py +824 -0
- fleet/state/inventory.py +360 -0
- fleet/state/invites.py +191 -0
- fleet/state/store.py +229 -0
- fleet/state/writes.py +286 -0
- fleet/ui.py +75 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agents-fleet
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Give Claude Code, Codex and Gemini every machine you have: a live GPU, VRAM, RAM and disk inventory across your servers, SSH access between them, and an MCP server.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lion-zhang/fleet
|
|
6
|
+
Project-URL: Documentation, https://github.com/lion-zhang/fleet/blob/main/docs/getting-started.md
|
|
7
|
+
Project-URL: Issues, https://github.com/lion-zhang/fleet/issues
|
|
8
|
+
Author: Lin Zhang
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,claude-code,codex,coding-agent,devops,gemini-cli,gpu,gpu-monitoring,homelab,inventory,mcp,mcp-server,mlops,nvidia,remote-execution,runpod,ssh,ssh-keys,vast-ai,vram
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: System :: Clustering
|
|
25
|
+
Classifier: Topic :: System :: Monitoring
|
|
26
|
+
Classifier: Topic :: System :: Systems Administration
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Requires-Dist: filelock>=3.13
|
|
29
|
+
Requires-Dist: mcp>=2
|
|
30
|
+
Requires-Dist: platformdirs>=4.2
|
|
31
|
+
Requires-Dist: psutil>=5.9
|
|
32
|
+
Requires-Dist: pyyaml>=6.0
|
|
33
|
+
Requires-Dist: rich>=13.7
|
|
34
|
+
Requires-Dist: typer>=0.12
|
|
35
|
+
Provides-Extra: mcp
|
|
36
|
+
Provides-Extra: migrate
|
|
37
|
+
Requires-Dist: pyrage>=1.1; extra == 'migrate'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
<!-- mcp-name: io.github.lion-zhang/fleet -->
|
|
41
|
+
<div align="center">
|
|
42
|
+
|
|
43
|
+
# fleet — let your coding agent use every machine you have
|
|
44
|
+
|
|
45
|
+
**Claude Code, Codex and Gemini see one machine: the one they run on.<br>
|
|
46
|
+
fleet shows them all of yours — every GPU, how much is free, and how to get there.**
|
|
47
|
+
|
|
48
|
+
[](https://github.com/lion-zhang/fleet/actions/workflows/test.yml)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+

|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
<img src="docs/assets/fleet-ls.svg" alt="fleet ls: free GPU, VRAM, CPU, RAM and disk across seven machines — an A100 rental with one busy and one idle card, an idle H100 rental flagged as costing money, a busy RTX 3090 box with a nearly full disk, a laptop, a NAS and a machine that is switched off" width="100%">
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
## The problem
|
|
58
|
+
|
|
59
|
+
Ask your agent to train a model and it starts on your laptop — while a 4090 sits idle
|
|
60
|
+
across the room and a rented A100 bills you by the hour. It cannot use what it cannot see.
|
|
61
|
+
|
|
62
|
+
- **Your agent is stuck on one machine.** It has no idea your other boxes exist.
|
|
63
|
+
- **Finding a free GPU is manual.** SSH into five hosts, run `nvidia-smi`, compare in your head.
|
|
64
|
+
- **Handing it a server means pasting credentials** into the chat, and hoping.
|
|
65
|
+
|
|
66
|
+
## Install once, then just talk to your agent
|
|
67
|
+
|
|
68
|
+
On the machine you work from:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -LsSf https://raw.githubusercontent.com/lion-zhang/fleet/main/install.sh | sh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
<sub>Windows: `powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lion-zhang/fleet/main/install.ps1 | iex"`</sub>
|
|
75
|
+
|
|
76
|
+
That is the whole setup. This machine becomes your fleet's **center**, and every
|
|
77
|
+
[supported agent](docs/agents.md) installed on it learns fleet. From here on you say what you want in plain words —
|
|
78
|
+
no commands to remember. When something is missing, the agent asks (illustrative):
|
|
79
|
+
|
|
80
|
+
> **You:** add my new GPU server
|
|
81
|
+
>
|
|
82
|
+
> **Agent:** Sure — how do you usually connect to it? An SSH command like
|
|
83
|
+
> `ssh -p 40001 root@1.2.3.4` is all I need.
|
|
84
|
+
>
|
|
85
|
+
> **You:** `ssh ubuntu@10.0.0.7`
|
|
86
|
+
>
|
|
87
|
+
> **Agent:** Added as `gpu-box`: 2× RTX 4090, both idle, 46 GB free. It's ready to use.
|
|
88
|
+
|
|
89
|
+
> **You:** train `train.py` on whatever has a free 24 GB card
|
|
90
|
+
>
|
|
91
|
+
> **Agent:** `rtx4090` has 23.1 GB free and an idle GPU; `a100-spot` is free too but costs
|
|
92
|
+
> $1.89/hr. Starting on `rtx4090`, logging to `train.log`.
|
|
93
|
+
|
|
94
|
+
| You say | What happens |
|
|
95
|
+
|---|---|
|
|
96
|
+
| "what's free right now?" | every machine checked, the free ones listed |
|
|
97
|
+
| "find me a box with a 24 GB card" | machines matched by what they have, not by name |
|
|
98
|
+
| "run the tests on the Linux box" | run there, results brought back |
|
|
99
|
+
| "what's costing me money?" | idle paid rentals flagged, with their hourly price |
|
|
100
|
+
| "let the laptop reach the NAS" | access granted, applied at once |
|
|
101
|
+
| "add a machine without typing its password" | a one-time line to paste there; it joins by itself |
|
|
102
|
+
|
|
103
|
+
No hostnames, keys or passwords go into the conversation, and anything irreversible
|
|
104
|
+
waits for your yes.
|
|
105
|
+
|
|
106
|
+
Every machine besides the center is a **member**. A member needs nothing installed —
|
|
107
|
+
just sshd. For machines where you also want to *run* fleet, or that you would rather not
|
|
108
|
+
type a password for, the agent gives you an invite line: pasted there, it installs fleet
|
|
109
|
+
and joins by itself.
|
|
110
|
+
|
|
111
|
+
### Already in your agent? Install from there
|
|
112
|
+
|
|
113
|
+
| Agent | Install |
|
|
114
|
+
|---|---|
|
|
115
|
+
| **Claude Code** | `/plugin marketplace add lion-zhang/fleet` then `/plugin install fleet@fleet` |
|
|
116
|
+
| **Codex** | `codex plugin marketplace add lion-zhang/fleet` then `codex plugin add fleet@fleet` |
|
|
117
|
+
| **Gemini CLI** | `gemini extensions install https://github.com/lion-zhang/fleet` |
|
|
118
|
+
| **GitHub Copilot CLI** | `copilot plugin marketplace add lion-zhang/fleet` then `copilot plugin install fleet@fleet` |
|
|
119
|
+
| **Cursor** | [](https://cursor.com/en/install-mcp?name=fleet&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJhZ2VudC1mbGVldCIsIm1jcCJdfQ%3D%3D) |
|
|
120
|
+
| **VS Code** | [](https://insiders.vscode.dev/redirect/mcp/install?name=fleet&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22agents-fleet%22%2C%22mcp%22%5D%7D) |
|
|
121
|
+
| **Claude Desktop** | open `fleet.mcpb` from the [latest release](https://github.com/lion-zhang/fleet/releases/latest) |
|
|
122
|
+
|
|
123
|
+
Plus OpenCode, Amp, Windsurf, Cline, Zed, Qwen Code, Goose, Kiro, Hermes — 35+ agents in all:
|
|
124
|
+
**[every agent →](docs/agents.md)**
|
|
125
|
+
|
|
126
|
+
On a machine in no fleet yet, these make it a center on first use, like the installer.
|
|
127
|
+
For a member, paste its invite line first.
|
|
128
|
+
|
|
129
|
+
## Built to be safe
|
|
130
|
+
|
|
131
|
+
- **Nothing to install on your machines.** fleet probes with one script over one SSH
|
|
132
|
+
connection — Linux, macOS or Windows. A NAS or a fresh rental works as it is.
|
|
133
|
+
- **Keys, not passwords.** A password, if needed at all, is typed once by you. Nothing
|
|
134
|
+
that could be stolen is stored, and the agent never sees a credential.
|
|
135
|
+
- **Access you control.** The center decides which machine may reach which, and a revoke
|
|
136
|
+
is pushed at once. `fleet access gpu-box --allow laptop`, `--deny` to take it back.
|
|
137
|
+
- **The agent asks, not guesses.** It is told to ask which machine you mean, and to leave
|
|
138
|
+
irreversible commands to you.
|
|
139
|
+
|
|
140
|
+
## Prefer the command line?
|
|
141
|
+
|
|
142
|
+
Everything the agent does is a plain `fleet` command, for when you want to drive it
|
|
143
|
+
yourself:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
fleet ls # every machine, with what is free right now
|
|
147
|
+
fleet ls --tag cuda --tag vram-24g # by capability: NVIDIA, a card of 24 GB or more
|
|
148
|
+
fleet top # live view, like htop for the whole fleet
|
|
149
|
+
fleet show gpu-box # one machine in detail: GPU processes, services, disks
|
|
150
|
+
fleet ssh gpu-box -- nvidia-smi # run something there
|
|
151
|
+
fleet add "ssh ubuntu@10.0.0.7" # add a machine; fleet invite NAME for a join line
|
|
152
|
+
fleet access nas --allow laptop # let one machine reach another
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
<img src="docs/assets/fleet-top.svg" alt="fleet top: a live view of GPU utilisation, free VRAM, CPU, RAM and disk across all machines" width="100%">
|
|
156
|
+
|
|
157
|
+
Rentals from vast.ai, RunPod or Lambda show their price (`fleet edit a100 --cost 1.89`),
|
|
158
|
+
the fleet's burn rate, and an alert when a paid machine sits idle. On Tailscale, ZeroTier
|
|
159
|
+
or WireGuard? fleet just needs an address it can route to.
|
|
160
|
+
|
|
161
|
+
## How fleet compares
|
|
162
|
+
|
|
163
|
+
fleet is about the machines you already have. It complements tools that launch new ones.
|
|
164
|
+
|
|
165
|
+
| | `ssh` + `nvidia-smi` | nvitop / gpustat | SkyPilot / dstack | **fleet** |
|
|
166
|
+
|---|:---:|:---:|:---:|:---:|
|
|
167
|
+
| All your machines in one view | ✗ | ✗ one machine | ✓ the ones it manages | ✓ |
|
|
168
|
+
| Built for coding agents (skill / MCP) | ✗ | ✗ | partly | ✓ |
|
|
169
|
+
| Nothing installed on target machines | ✓ | ✗ | ✗ | ✓ |
|
|
170
|
+
| Manages SSH access between machines | ✗ | ✗ | for its own clusters | ✓ |
|
|
171
|
+
| Launches new cloud VMs | ✗ | ✗ | ✓ | ✗ |
|
|
172
|
+
|
|
173
|
+
## FAQ
|
|
174
|
+
|
|
175
|
+
<details>
|
|
176
|
+
<summary><b>I use Claude Code <i>and</i> Codex (and more) on one machine. Does that work?</b></summary>
|
|
177
|
+
|
|
178
|
+
Yes — that is the normal case. fleet is installed once per machine: one command, one
|
|
179
|
+
inventory, one set of keys. Each agent only gets a small skill or MCP entry pointing at
|
|
180
|
+
it, so Claude Code, Codex, Gemini CLI and a desktop app all see the same machines, and can
|
|
181
|
+
use them at the same time. Install a new agent later? Run `fleet setup` (or ask an agent
|
|
182
|
+
that already has fleet to do it).
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary><b>What does my agent actually get?</b></summary>
|
|
187
|
+
|
|
188
|
+
Agents with a shell (Claude Code, Codex, Gemini CLI, Copilot CLI, OpenCode, …) get a
|
|
189
|
+
skill — text that tells them the `fleet` commands; it costs nothing until a task needs a
|
|
190
|
+
machine. Apps that cannot run commands (Claude Desktop, Cursor, VS Code, …) get an MCP
|
|
191
|
+
server that runs the same commands for them. The installer sets up the supported agents
|
|
192
|
+
you have; [docs/agents.md](docs/agents.md) has the details for each.
|
|
193
|
+
</details>
|
|
194
|
+
|
|
195
|
+
<details>
|
|
196
|
+
<summary><b>Does it work behind NAT, or across sites?</b></summary>
|
|
197
|
+
|
|
198
|
+
The center needs an address it can route to: a public IP, a LAN, or an overlay such as
|
|
199
|
+
Tailscale. A machine the center cannot dial can still join with the `fleet invite` line
|
|
200
|
+
and report in; granting access to it waits until the center can reach it.
|
|
201
|
+
</details>
|
|
202
|
+
|
|
203
|
+
<details>
|
|
204
|
+
<summary><b>What if the center is off?</b></summary>
|
|
205
|
+
|
|
206
|
+
Normal — it can be a laptop that is closed half the day. Everything already granted keeps
|
|
207
|
+
working; only changes wait for it. Move the role with `fleet center NAME`.
|
|
208
|
+
</details>
|
|
209
|
+
|
|
210
|
+
<details>
|
|
211
|
+
<summary><b>Windows?</b></summary>
|
|
212
|
+
|
|
213
|
+
Yes, both ways. A Windows machine works as a target with OpenSSH Server and nothing else,
|
|
214
|
+
and fleet runs on Windows too, center included: interactive `fleet ssh`, `fleet top` and
|
|
215
|
+
the background service all work there. See [Windows](docs/getting-started.md#windows).
|
|
216
|
+
</details>
|
|
217
|
+
|
|
218
|
+
## Learn more
|
|
219
|
+
|
|
220
|
+
- [Getting started](docs/getting-started.md) — the full walkthrough, every command
|
|
221
|
+
- [Every agent](docs/agents.md) — install commands and config for 35+ agents
|
|
222
|
+
- [Design](docs/design/layers.md) — one core per machine, a skill per agent, MCP for the rest; and [access](docs/design/access.md) — how access is granted, signed and revoked
|
|
223
|
+
|
|
224
|
+
**Status:** v0.5. The test suite runs on Linux, macOS and Windows, and every command is
|
|
225
|
+
run end to end on a real machine of each OS in CI — from a script, as an agent runs it,
|
|
226
|
+
and at a real terminal, as you do. Multi-machine fleets (key, password, invite, handover)
|
|
227
|
+
are tested on Linux machines built from scratch.
|
|
228
|
+
|
|
229
|
+
Issues and pull requests are welcome — `uv run pytest -q` runs the tests. If fleet saved
|
|
230
|
+
you a GPU-hour, a ⭐ helps other people find it.
|
|
231
|
+
|
|
232
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
fleet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
fleet/__main__.py,sha256=pcWnqKWwe-Zh_hU0-4-VQarnkKz2STKlc7C6BAGhJj0,718
|
|
3
|
+
fleet/cli.py,sha256=FDypkR5zRwG6MgwD7fTTqYcdFKXH8QoLrZok3j35TTs,103354
|
|
4
|
+
fleet/config.py,sha256=D1ZX6T8NjaoKyuAMA20wAWoZdK1ukTBKbHBoBIuVkUs,3901
|
|
5
|
+
fleet/edit.py,sha256=L73Kk8e66_bOAxWQiBlTGu8niqOVWEOa80yU7HK53qw,7652
|
|
6
|
+
fleet/install.py,sha256=ma1yQlVidHBm0HiBerg7WKqPp_jlMMPqv8-X8PY7KvE,19829
|
|
7
|
+
fleet/links.py,sha256=tn3V8Yk4sFOHFCnwb_ibL6zz5zKaS3khGVee7K9XClk,1152
|
|
8
|
+
fleet/mcpserver.py,sha256=sQTeb1E6270H9t79FbML6MknZAjmp3sFOWCtIfjiToE,14650
|
|
9
|
+
fleet/models.py,sha256=GTAPhl7zrDCBZR4o-1QRel-WGBUb-9mIjhhPc89ACnE,7820
|
|
10
|
+
fleet/onboard.py,sha256=n0BllNRcAPc7-A9Y49aFLIrE5JR9J9P0M_NygmB_8ss,7216
|
|
11
|
+
fleet/reconcile.py,sha256=NyWlIksTjNX_g_r0QpEJ97fazq2o5DyJq2_955R_FbY,10805
|
|
12
|
+
fleet/secrets.py,sha256=vAgIqyQGEtrO08D2aYNEZT6QfXYQFaySHershWnPTNk,2069
|
|
13
|
+
fleet/serve.py,sha256=yCec_TzOVdA7l9Uo0Z4I6ILDqMplsYSZPY1bBSsdf0M,9180
|
|
14
|
+
fleet/service.py,sha256=OrR3ShTZ1j1bzDfTtOTgNj00_AQasSHGZ0AEpAaMwwU,17758
|
|
15
|
+
fleet/ui.py,sha256=CrlkNS7RLX0gqYB_JDklVXkDWQgNJgj-Gr0M1GQ0eys,3182
|
|
16
|
+
fleet/agents/__init__.py,sha256=MaYfaDlE8mltM6F9ORs67ISVkFZwNmwMLMq5wrA-Et0,2057
|
|
17
|
+
fleet/agents/clients.py,sha256=ishgW11a5SY30NneGOiV8RZFxD_Riw1w4XYkwwL2JaE,6071
|
|
18
|
+
fleet/agents/docs.py,sha256=Sx3S8j9_n9cLd2MV2zDFtntfAD77ecqAtmQPpwY6mck,11085
|
|
19
|
+
fleet/agents/registry.py,sha256=QUyXLpW91ufFZV6zEmfhI7jxUA6QqHMck-Ziv9-LzBY,15694
|
|
20
|
+
fleet/agents/usage.py,sha256=M90Sr1Sf6ZYgEe2HPhJKo2sEjKA6nwWEffD9KAMNvno,14725
|
|
21
|
+
fleet/ops/__init__.py,sha256=qLpw1D33WFBUGCE03nzVmnSTFZu0Ms5l1_ZkquSPceM,827
|
|
22
|
+
fleet/ops/enrol.py,sha256=c9sRzEBB4j6FL2SfJW8HE_dat7VU7WCng2xjA9x-PMk,10039
|
|
23
|
+
fleet/ops/errors.py,sha256=UPA1He0f8n4ADnuYUnpyHktcX7sY4ib-8IJtdLkKKm0,970
|
|
24
|
+
fleet/ops/firstrun.py,sha256=YHMI_-8bb0wXP9cN-oeIc48b-iYfN-K_2R1bwQd-1RU,5436
|
|
25
|
+
fleet/ops/handover.py,sha256=85CruDtTo5ZyMhH2Ly65XeS_qBr5NWrSaPtWTzS6DLQ,17835
|
|
26
|
+
fleet/ops/identity.py,sha256=5JPjPs18Lceuj-dyZun4MIvbWK5E8nPpDL1uQ02t04o,2689
|
|
27
|
+
fleet/ops/join.py,sha256=MdX3fPjHQlDdJ2PyYJk-2pnPdSfG5QP2bqvprKJUpwk,20792
|
|
28
|
+
fleet/ops/lifecycle.py,sha256=4gu1RlDcvexuD2GJX5XmU2BPmYP8Zulx1iB75zWMZAQ,8920
|
|
29
|
+
fleet/ops/member.py,sha256=E3UawMcHO_P8tOvz08_No4u977QL_ALGTsOZ_v_3BzM,5649
|
|
30
|
+
fleet/ops/migrate.py,sha256=3Wuds8_uc9wV8uFklytcmW_JPDhSigLO34VBn_7UE1A,3814
|
|
31
|
+
fleet/ops/names.py,sha256=_8XzlEbNu4F41372Fh14ViVX2s0uN40J9-wGaoUPpKA,632
|
|
32
|
+
fleet/ops/rows.py,sha256=OFFXqdr4S1EBIs7I5EYSaY_qHbYn0ER1iA6xm4O11Jk,5312
|
|
33
|
+
fleet/ops/sweep.py,sha256=lczAIRq6CgJVSSM2bCI0BV2vf4Yo494N-oyX7m2wl5U,19397
|
|
34
|
+
fleet/ops/sync.py,sha256=n5d44CypxpcyI_A3vI8RrXoe7n98Jyg3848tm4Mrocc,13388
|
|
35
|
+
fleet/probe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
+
fleet/probe/parse.py,sha256=auJnzACu_bMZny0Y2-dhyN0LmKqvUf_z6h3lHYW3s14,8880
|
|
37
|
+
fleet/probe/runner.py,sha256=pzh7XZDyrqIDn49ifVRQYzpsZFxg2UBnWoG4vey3XFU,19503
|
|
38
|
+
fleet/render/__init__.py,sha256=cboMaOyK6kmAK6I8oCuYWDTwMhlkmtlbADaJvvunrRY,608
|
|
39
|
+
fleet/render/staleness.py,sha256=_zRjqbaU86Hu9ksKFCLEUb0TrG7Pm1l4uSpeWX0ZMQ0,1275
|
|
40
|
+
fleet/render/top.py,sha256=TjJiv-lm1FJQqtnqxUQHhzWYJwcL7fvTx09l5A9vmro,18691
|
|
41
|
+
fleet/render/view.py,sha256=cUzKwfpfonkqU1FGqytwGPrlWtZkVROigxh0HOC8dd8,22158
|
|
42
|
+
fleet/ssh/__init__.py,sha256=XuviBSkS5oTGZ0BFCxw5oxeE6mLEe6Nji_1zEGigOY0,848
|
|
43
|
+
fleet/ssh/auth.py,sha256=vsP2IJRMn-vCpZSR6SjujFbRHbHtyM2VJPXiQ6de-7I,3188
|
|
44
|
+
fleet/ssh/authkeys.py,sha256=fMHgvbDeXEhhVxtO3CMzAAj3Ltj1EE_xJ04LxuFtu-w,10541
|
|
45
|
+
fleet/ssh/cmd.py,sha256=kOY1qYR75hOZ5iurA8L3vRuJcfJwO-SIr3N3xDSI15Y,17956
|
|
46
|
+
fleet/ssh/keys.py,sha256=Ff-Gj12esE-BppS9SafGBx6j_RKXhvjdU78qyrRN_Mc,15477
|
|
47
|
+
fleet/state/__init__.py,sha256=YSY_cpM-UbWkH6Y80QqujYGIefGZszrr7kFPp5eu8oc,1008
|
|
48
|
+
fleet/state/access.py,sha256=QfflWbS-ZnFdNuoHS-fBC0H9qPtMgeXZgUrn3flyzJs,37249
|
|
49
|
+
fleet/state/inventory.py,sha256=GM_0JOC4GyuLegFZ7kVnh6KmedKcF79dhUk1LSCjlxI,14562
|
|
50
|
+
fleet/state/invites.py,sha256=BMUQN6y0kIXvDg3ImFlJ9W7oSOYAEnX3spFsf-Ru2lo,7855
|
|
51
|
+
fleet/state/store.py,sha256=2Je6WAsMuwE2tlyPzjzDohh4xioaLTkDuaxGhuDBGnc,11042
|
|
52
|
+
fleet/state/writes.py,sha256=bCr8NSUR_hZBHx-Zm10ewfCIfhQnctIutBLso2QMFZ4,10801
|
|
53
|
+
fleet/probe/payload.ps1,sha256=_ZPm6ls0pRhY6RxvSiZL4YBtW8Ihhx2KUWuzUJgzA2E,4125
|
|
54
|
+
fleet/probe/payload.sh,sha256=icutxA4SxqKa88iekOnH7eLT-3-o_hk9l15yTGt0m8A,11720
|
|
55
|
+
agents_fleet-0.5.0.dist-info/METADATA,sha256=BVyylRHyzOlJqeemHgWaGinhxZiOjtUh3eNssymqZKg,11708
|
|
56
|
+
agents_fleet-0.5.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
57
|
+
agents_fleet-0.5.0.dist-info/entry_points.txt,sha256=YYX5tkHZMV0tl_c17hhMGkZMSYTuzTfo4STfYW0OYEM,71
|
|
58
|
+
agents_fleet-0.5.0.dist-info/licenses/LICENSE,sha256=7BGi71c6Zfu63DGQNGnNnNMOnsw8guNlSUJsvFWWqpc,1066
|
|
59
|
+
agents_fleet-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lin Zhang
|
|
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.
|
fleet/__init__.py
ADDED
|
File without changes
|
fleet/__main__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""`python -m fleet`, which exists so the center can serve without a console window.
|
|
2
|
+
|
|
3
|
+
Windows shows a console for any program built against the console subsystem, and the
|
|
4
|
+
`fleet` launcher uv writes is one. `pythonw.exe` is the same interpreter built against
|
|
5
|
+
the GUI subsystem, so a scheduled task that runs `pythonw -m fleet center --listen` gets
|
|
6
|
+
no window at all -- not a hidden one, and not one that flashes on the way past.
|
|
7
|
+
|
|
8
|
+
`-m` rather than a second console script: uv generates launchers from the entry points
|
|
9
|
+
in pyproject.toml, and asking it for a windowed variant means asking for a packaging
|
|
10
|
+
feature it does not have. The interpreter is already there next to the one in use.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .cli import main
|
|
14
|
+
|
|
15
|
+
main()
|
fleet/agents/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Teaching agents what fleet is, on whatever surface they read.
|
|
2
|
+
|
|
3
|
+
**This is the only part of fleet that writes outside fleet's own directories.** That
|
|
4
|
+
sentence governed one module before the split and governs two now -- `docs` writes skill
|
|
5
|
+
files and marker-delimited regions in AGENTS.md, `clients` writes MCP config JSON -- so
|
|
6
|
+
it lives here, where both are in scope.
|
|
7
|
+
|
|
8
|
+
The rule it implies is the same in both: never touch a byte we did not write. Files we
|
|
9
|
+
fully own are overwritten wholesale; files that belong to the user get a delimited region
|
|
10
|
+
or a single key, and nothing else in them is disturbed.
|
|
11
|
+
|
|
12
|
+
`registry` is the data table the other two read, and `usage` is the prose they install.
|
|
13
|
+
Supporting a new agent should be a row in `registry`, not a branch anywhere else.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .clients import (apply_mcp, detect_mcp_clients, install_mcp, installed_mcp_clients,
|
|
19
|
+
mcp_entry, remove_mcp, stale_mcp_clients, uninstall_mcp)
|
|
20
|
+
from .docs import (BEGIN, END, Change, apply_block, detect_targets, install,
|
|
21
|
+
installed_targets, legacy_paths, plan, remove_block, stale_targets,
|
|
22
|
+
uninstall)
|
|
23
|
+
from .registry import (AGENTS, BY_NAME, HERMES_CATEGORY, MCP_CLIENTS, TARGETS, Agent,
|
|
24
|
+
McpClient, config_command, fleet_command, fleet_executable,
|
|
25
|
+
package_version)
|
|
26
|
+
from .usage import agents_block, dist_skill_text, hermes_skill_text, skill_text, stamp
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AGENTS", "BEGIN", "BY_NAME", "END", "HERMES_CATEGORY", "MCP_CLIENTS", "TARGETS",
|
|
30
|
+
"Agent", "Change", "McpClient", "agents_block", "apply_block", "apply_mcp",
|
|
31
|
+
"config_command", "detect_mcp_clients", "detect_targets", "dist_skill_text", "fleet_command",
|
|
32
|
+
"fleet_executable", "hermes_skill_text",
|
|
33
|
+
"install", "install_mcp", "installed_mcp_clients", "installed_targets",
|
|
34
|
+
"legacy_paths", "stale_mcp_clients", "stale_targets", "mcp_entry", "package_version", "plan",
|
|
35
|
+
"remove_block", "remove_mcp", "skill_text", "stamp", "uninstall", "uninstall_mcp",
|
|
36
|
+
]
|
fleet/agents/clients.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Registering fleet with the desktop clients that speak MCP rather than read a file.
|
|
2
|
+
|
|
3
|
+
The other half of `fleet setup`, and it writes JSON someone else owns -- a client's
|
|
4
|
+
config file, which may hold servers fleet knows nothing about. So it edits one key under
|
|
5
|
+
`mcpServers` and leaves the document otherwise exactly as found, including the keys and
|
|
6
|
+
the formatting.
|
|
7
|
+
|
|
8
|
+
Same rule as `docs.py`, arrived at from the other direction: never touch a byte we did
|
|
9
|
+
not write.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from ..state.writes import replace_file
|
|
19
|
+
from .registry import MCP_CLIENTS, UVX, Change, fleet_executable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def mcp_entry(cmd: str) -> dict:
|
|
23
|
+
"""What we register. `fleet mcp` serves stdio, which is how clients launch a server."""
|
|
24
|
+
if cmd == UVX:
|
|
25
|
+
import shutil
|
|
26
|
+
|
|
27
|
+
# Absolute when we can: a desktop client's PATH is not a shell's.
|
|
28
|
+
return {"command": shutil.which("uvx") or "uvx", "args": ["agents-fleet", "mcp"]}
|
|
29
|
+
return {"command": cmd, "args": ["mcp"]}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def apply_mcp(existing: str, key: str, cmd: str) -> str:
|
|
34
|
+
"""Add or update our server in a client's config, leaving every other byte alone.
|
|
35
|
+
|
|
36
|
+
Same promise as the markdown region, kept a different way: the document is parsed,
|
|
37
|
+
exactly one key is set, and it is written back. A config we cannot parse is returned
|
|
38
|
+
untouched -- refusing to guess is the only safe move on a file we did not write and
|
|
39
|
+
that may hold someone else's tokens.
|
|
40
|
+
"""
|
|
41
|
+
import json
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
doc = json.loads(existing) if existing.strip() else {}
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
return existing
|
|
47
|
+
if not isinstance(doc, dict):
|
|
48
|
+
return existing
|
|
49
|
+
servers = doc.get(key)
|
|
50
|
+
if not isinstance(servers, dict):
|
|
51
|
+
servers = {}
|
|
52
|
+
servers["fleet"] = mcp_entry(cmd)
|
|
53
|
+
doc[key] = servers
|
|
54
|
+
return json.dumps(doc, indent=2) + "\n"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def remove_mcp(existing: str, key: str) -> str:
|
|
59
|
+
"""Drop our server. A config that never had one comes back untouched."""
|
|
60
|
+
import json
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
doc = json.loads(existing) if existing.strip() else {}
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
return existing
|
|
66
|
+
if not isinstance(doc, dict) or not isinstance(doc.get(key), dict):
|
|
67
|
+
return existing
|
|
68
|
+
if doc[key].pop("fleet", None) is None:
|
|
69
|
+
return existing
|
|
70
|
+
return json.dumps(doc, indent=2) + "\n"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def detect_mcp_clients(root: Path, platform: str = "") -> list[str]:
|
|
75
|
+
"""MCP clients that have actually run here. Judged by the directory their config
|
|
76
|
+
lives in, because the file itself does not exist until one is configured."""
|
|
77
|
+
platform = platform or sys.platform
|
|
78
|
+
out = []
|
|
79
|
+
for c in MCP_CLIENTS:
|
|
80
|
+
path = c.path(root, platform)
|
|
81
|
+
if path and path.parent.is_dir():
|
|
82
|
+
out.append(c.name)
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def installed_mcp_clients(root: Path, platform: str = "") -> list[str]:
|
|
88
|
+
"""Clients that already have fleet registered -- the only ones a refresh touches."""
|
|
89
|
+
platform = platform or sys.platform
|
|
90
|
+
out = []
|
|
91
|
+
for c in MCP_CLIENTS:
|
|
92
|
+
path = c.path(root, platform)
|
|
93
|
+
if not path or not path.exists():
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
doc = json.loads(path.read_text(encoding="utf-8") or "{}")
|
|
97
|
+
except (OSError, ValueError):
|
|
98
|
+
continue
|
|
99
|
+
if isinstance(doc, dict) and isinstance(doc.get(c.key), dict) \
|
|
100
|
+
and "fleet" in doc[c.key]:
|
|
101
|
+
out.append(c.name)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def stale_mcp_clients(root: Path, cmd: str, platform: str = "") -> list[str]:
|
|
106
|
+
"""Registered clients whose entry points somewhere other than this fleet."""
|
|
107
|
+
platform = platform or sys.platform
|
|
108
|
+
out = []
|
|
109
|
+
for c in MCP_CLIENTS:
|
|
110
|
+
if c.name not in installed_mcp_clients(root, platform):
|
|
111
|
+
continue
|
|
112
|
+
path = c.path(root, platform)
|
|
113
|
+
current = path.read_text(encoding="utf-8")
|
|
114
|
+
if apply_mcp(current, c.key, cmd) != current:
|
|
115
|
+
out.append(c.name)
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def install_mcp(root: Path, clients: list[str], cmd: str, *,
|
|
120
|
+
dry_run: bool = False, platform: str = "") -> list[Change]:
|
|
121
|
+
"""Register `fleet mcp` with each client, without disturbing its other servers."""
|
|
122
|
+
platform = platform or sys.platform
|
|
123
|
+
changes: list[Change] = []
|
|
124
|
+
for client in MCP_CLIENTS:
|
|
125
|
+
if client.name not in clients:
|
|
126
|
+
continue
|
|
127
|
+
path = client.path(root, platform)
|
|
128
|
+
if path is None:
|
|
129
|
+
continue
|
|
130
|
+
current = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
131
|
+
desired = apply_mcp(current, client.key, cmd)
|
|
132
|
+
if desired == current and current.strip() and "fleet" not in current:
|
|
133
|
+
# apply_mcp returns the file untouched when it cannot parse it, which is the
|
|
134
|
+
# right thing to do to someone else's config and the wrong thing to report
|
|
135
|
+
# as "unchanged" -- that reads as already set up.
|
|
136
|
+
changes.append(Change(client.name, path, "unreadable"))
|
|
137
|
+
continue
|
|
138
|
+
action = ("unchanged" if desired == current
|
|
139
|
+
else "created" if not current else "updated")
|
|
140
|
+
if action != "unchanged" and not dry_run:
|
|
141
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
replace_file(path, desired)
|
|
143
|
+
changes.append(Change(client.name, path, action))
|
|
144
|
+
return changes
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def uninstall_mcp(root: Path, clients: list[str], *,
|
|
149
|
+
dry_run: bool = False, platform: str = "") -> list[Change]:
|
|
150
|
+
platform = platform or sys.platform
|
|
151
|
+
changes: list[Change] = []
|
|
152
|
+
for client in MCP_CLIENTS:
|
|
153
|
+
if client.name not in clients:
|
|
154
|
+
continue
|
|
155
|
+
path = client.path(root, platform)
|
|
156
|
+
if path is None or not path.exists():
|
|
157
|
+
continue
|
|
158
|
+
current = path.read_text(encoding="utf-8")
|
|
159
|
+
desired = remove_mcp(current, client.key)
|
|
160
|
+
action = "removed" if desired != current else "unchanged"
|
|
161
|
+
if action == "removed" and not dry_run:
|
|
162
|
+
replace_file(path, desired)
|
|
163
|
+
changes.append(Change(client.name, path, action))
|
|
164
|
+
return changes
|
|
165
|
+
|