swarph-cli 0.16.0__tar.gz → 0.18.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.
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/PKG-INFO +33 -1
- swarph_cli-0.16.0/src/swarph_cli.egg-info/PKG-INFO → swarph_cli-0.18.0/README.md +28 -40
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/pyproject.toml +10 -1
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.18.0/src/swarph_cli/commands/brain.py +74 -0
- swarph_cli-0.18.0/src/swarph_cli/commands/service.py +74 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/main.py +2 -0
- swarph_cli-0.18.0/src/swarph_cli/service/__init__.py +0 -0
- swarph_cli-0.18.0/src/swarph_cli/service/app.py +53 -0
- swarph_cli-0.18.0/src/swarph_cli/service/providers.py +87 -0
- swarph_cli-0.16.0/README.md → swarph_cli-0.18.0/src/swarph_cli.egg-info/PKG-INFO +72 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli.egg-info/SOURCES.txt +9 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli.egg-info/requires.txt +5 -0
- swarph_cli-0.18.0/tests/test_brain_command.py +50 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_caller_meta_guard.py +8 -6
- swarph_cli-0.18.0/tests/test_service_app.py +57 -0
- swarph_cli-0.18.0/tests/test_service_command.py +53 -0
- swarph_cli-0.18.0/tests/test_service_providers.py +97 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/LICENSE +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/setup.cfg +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_brain_ask_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_init_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_main.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_security.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.16.0 → swarph_cli-0.18.0}/tests/test_watchdog_stale_peers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
@@ -36,6 +36,10 @@ Requires-Dist: fastapi>=0.115; extra == "gateway"
|
|
|
36
36
|
Requires-Dist: uvicorn[standard]>=0.32; extra == "gateway"
|
|
37
37
|
Requires-Dist: pydantic>=2.9; extra == "gateway"
|
|
38
38
|
Requires-Dist: croniter>=6.2; extra == "gateway"
|
|
39
|
+
Provides-Extra: service
|
|
40
|
+
Requires-Dist: fastapi>=0.115; extra == "service"
|
|
41
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == "service"
|
|
42
|
+
Requires-Dist: pydantic>=2.9; extra == "service"
|
|
39
43
|
Dynamic: license-file
|
|
40
44
|
|
|
41
45
|
# swarph-cli
|
|
@@ -65,7 +69,9 @@ This is one of three repos in the v0.3.x architecture:
|
|
|
65
69
|
swarph "prompt" one-shot against any provider (claude/openai/gemini/deepseek/grok)
|
|
66
70
|
swarph chat interactive REPL — multi-turn, slash commands, live provider switch
|
|
67
71
|
swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited answer or raw chunks
|
|
72
|
+
swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
|
|
68
73
|
swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
|
|
74
|
+
swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
|
|
69
75
|
swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
|
|
70
76
|
swarph spawn <role> launch a long-lived agent session as a named mesh cell
|
|
71
77
|
swarph daemon foreground inbox-drain loop (the mesh doorbell)
|
|
@@ -92,6 +98,18 @@ $ swarph brain-ask --no-synth --limit 3 "cross-vendor fallback governor order"
|
|
|
92
98
|
|
|
93
99
|
Config is via env, mirroring `swarph mesh`'s token model: `GBRAIN_MCP_URL` or `SWARPH_BRAIN_MCP` (gbrain endpoint; defaults to `http://127.0.0.1:8792/mcp`), and the read token resolved `--token-file` > `GBRAIN_TOKEN` > `SWARPH_BRAIN_TOKEN` > the mesh per-peer token (`~/.config/swarph/<self>.peer_token`) — so the mesh peer token doubles as the gbrain read token. Optional `SWARPH_FACADE` / `SWARPH_FACADE_TOKEN` enable the $0 cited-synthesis pass; without them, brain-ask prints the raw ranked chunks.
|
|
94
100
|
|
|
101
|
+
### `swarph brain serve`
|
|
102
|
+
|
|
103
|
+
`brain-ask` *searches* a running brain; **`brain serve`** *runs* one. gbrain (the sovereign $0 semantic-memory server) is an external binary — this verb is a thin launcher that applies the swarph-blessed defaults and replaces itself with `gbrain serve`:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
$ swarph brain serve # 127.0.0.1:8792, 1-year token TTL
|
|
107
|
+
$ swarph brain serve --bind 100.x.y.z --port 8792 # expose on a tailnet IP
|
|
108
|
+
$ swarph brain serve --gbrain-bin /opt/gbrain # explicit binary (else GBRAIN_BIN / PATH)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Defaults are loopback bind (expose a tailnet IP explicitly) and a **1-year token TTL** — a short TTL silently 401s long-lived mesh cells, a lesson learned the hard way. If `gbrain` isn't on `PATH` the verb prints an install hint and exits `2`; `brain-ask` still queries any already-running brain.
|
|
112
|
+
|
|
95
113
|
### `swarph gateway`
|
|
96
114
|
|
|
97
115
|
The **mesh-gateway** is the coordination/DM server every other verb talks to — the peer registry, the DM inbox/outbox, feature aggregation with allowlist + caps, and lane/service control. It used to be a separate deployment; it's now bundled, so any host can stand one up with a single command. It pairs with the client verbs (`mesh`, `spawn`, `daemon`, `watchdog`) and with `brain-ask`/gbrain to form the mesh's coordination plane.
|
|
@@ -106,6 +124,20 @@ $ swarph gateway serve --host 100.x.y.z --port 8788 # expose on a tailnet IP
|
|
|
106
124
|
|
|
107
125
|
`--token` sets the bearer (`MESH_GATEWAY_TOKEN`) for the served process; omit it and an existing env token is used, or a fresh one is minted and printed once so you can hand it to the mesh cells. `--db PATH` points the gateway at a specific SQLite file (`MESH_DB_PATH`). Run without the extra and the verb prints a `pip install "swarph-cli[gateway]"` hint and exits.
|
|
108
126
|
|
|
127
|
+
### `swarph service`
|
|
128
|
+
|
|
129
|
+
Turn a *subscription* LLM CLI into a **$0 HTTP lane** the mesh can call — `swarph service serve --provider <claude|codex|gemini>` runs a small FastAPI app exposing `POST /delegate` (bearer-authed) + `GET /health`:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
$ pip install "swarph-cli[service]"
|
|
133
|
+
$ swarph service serve --provider claude # 127.0.0.1:8799, mints+prints a token
|
|
134
|
+
$ swarph service serve --provider gemini --host 100.x.y.z --port 8799 # expose on a tailnet IP
|
|
135
|
+
$ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
|
|
136
|
+
-d '{"prompt":"summarise this in one line: ..."}' # -> {"text": "...", "cost_usd": 0.0}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The load-bearing piece is **billing-scrub**: the subprocess env has every known provider API-key var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, …) stripped, so the wrapped CLI can *only* use its $0 subscription auth — it can never silently fall back to metered API billing. Default loopback bind (expose a tailnet IP explicitly); the bearer is `--token` > `SWARPH_SERVICE_TOKEN` > a freshly minted+printed token. Note the `gemini` (agy) lane reads the prompt from argv only and hard-caps its length (~4 KB) — very long prompts are rejected, not truncated. Run without the extra and the verb prints a `pip install "swarph-cli[service]"` hint and exits.
|
|
140
|
+
|
|
109
141
|
### `swarph highlight`
|
|
110
142
|
|
|
111
143
|
The swarph **timeline** is an append-only, multi-author `TIMELINE.md` — a git-backed continuous-learning log where every cell's highlights converge into one file. `swarph highlight` is the mechanics; *you* (or your agent) decide what's worth logging and which memory it links to.
|
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: swarph-cli
|
|
3
|
-
Version: 0.16.0
|
|
4
|
-
Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
|
|
5
|
-
Author: Pierre Samson, Claude Opus
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/BrainSurfing-tech/swarph-cli
|
|
8
|
-
Project-URL: Source, https://github.com/BrainSurfing-tech/swarph-cli
|
|
9
|
-
Project-URL: Substrate, https://github.com/BrainSurfing-tech/swarph-mesh
|
|
10
|
-
Project-URL: Spec, https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md
|
|
11
|
-
Keywords: swarph,llm,cli,mesh,gemini,claude,deepseek
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
-
Classifier: Topic :: Utilities
|
|
24
|
-
Requires-Python: >=3.10
|
|
25
|
-
Description-Content-Type: text/markdown
|
|
26
|
-
License-File: LICENSE
|
|
27
|
-
Requires-Dist: swarph-mesh>=0.5.0
|
|
28
|
-
Requires-Dist: swarph-shared>=0.3.3
|
|
29
|
-
Requires-Dist: PyYAML>=6.0
|
|
30
|
-
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
-
Provides-Extra: mcp
|
|
33
|
-
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
34
|
-
Provides-Extra: gateway
|
|
35
|
-
Requires-Dist: fastapi>=0.115; extra == "gateway"
|
|
36
|
-
Requires-Dist: uvicorn[standard]>=0.32; extra == "gateway"
|
|
37
|
-
Requires-Dist: pydantic>=2.9; extra == "gateway"
|
|
38
|
-
Requires-Dist: croniter>=6.2; extra == "gateway"
|
|
39
|
-
Dynamic: license-file
|
|
40
|
-
|
|
41
1
|
# swarph-cli
|
|
42
2
|
|
|
43
3
|
**One CLI for every LLM.** Drive Claude, GPT, Gemini, DeepSeek, and Grok from a single binary — then connect them into a coordinating *mesh* where agents from different vendors talk to each other through a shared protocol, each staying itself.
|
|
@@ -65,7 +25,9 @@ This is one of three repos in the v0.3.x architecture:
|
|
|
65
25
|
swarph "prompt" one-shot against any provider (claude/openai/gemini/deepseek/grok)
|
|
66
26
|
swarph chat interactive REPL — multi-turn, slash commands, live provider switch
|
|
67
27
|
swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited answer or raw chunks
|
|
28
|
+
swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
|
|
68
29
|
swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
|
|
30
|
+
swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
|
|
69
31
|
swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
|
|
70
32
|
swarph spawn <role> launch a long-lived agent session as a named mesh cell
|
|
71
33
|
swarph daemon foreground inbox-drain loop (the mesh doorbell)
|
|
@@ -92,6 +54,18 @@ $ swarph brain-ask --no-synth --limit 3 "cross-vendor fallback governor order"
|
|
|
92
54
|
|
|
93
55
|
Config is via env, mirroring `swarph mesh`'s token model: `GBRAIN_MCP_URL` or `SWARPH_BRAIN_MCP` (gbrain endpoint; defaults to `http://127.0.0.1:8792/mcp`), and the read token resolved `--token-file` > `GBRAIN_TOKEN` > `SWARPH_BRAIN_TOKEN` > the mesh per-peer token (`~/.config/swarph/<self>.peer_token`) — so the mesh peer token doubles as the gbrain read token. Optional `SWARPH_FACADE` / `SWARPH_FACADE_TOKEN` enable the $0 cited-synthesis pass; without them, brain-ask prints the raw ranked chunks.
|
|
94
56
|
|
|
57
|
+
### `swarph brain serve`
|
|
58
|
+
|
|
59
|
+
`brain-ask` *searches* a running brain; **`brain serve`** *runs* one. gbrain (the sovereign $0 semantic-memory server) is an external binary — this verb is a thin launcher that applies the swarph-blessed defaults and replaces itself with `gbrain serve`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
$ swarph brain serve # 127.0.0.1:8792, 1-year token TTL
|
|
63
|
+
$ swarph brain serve --bind 100.x.y.z --port 8792 # expose on a tailnet IP
|
|
64
|
+
$ swarph brain serve --gbrain-bin /opt/gbrain # explicit binary (else GBRAIN_BIN / PATH)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Defaults are loopback bind (expose a tailnet IP explicitly) and a **1-year token TTL** — a short TTL silently 401s long-lived mesh cells, a lesson learned the hard way. If `gbrain` isn't on `PATH` the verb prints an install hint and exits `2`; `brain-ask` still queries any already-running brain.
|
|
68
|
+
|
|
95
69
|
### `swarph gateway`
|
|
96
70
|
|
|
97
71
|
The **mesh-gateway** is the coordination/DM server every other verb talks to — the peer registry, the DM inbox/outbox, feature aggregation with allowlist + caps, and lane/service control. It used to be a separate deployment; it's now bundled, so any host can stand one up with a single command. It pairs with the client verbs (`mesh`, `spawn`, `daemon`, `watchdog`) and with `brain-ask`/gbrain to form the mesh's coordination plane.
|
|
@@ -106,6 +80,20 @@ $ swarph gateway serve --host 100.x.y.z --port 8788 # expose on a tailnet IP
|
|
|
106
80
|
|
|
107
81
|
`--token` sets the bearer (`MESH_GATEWAY_TOKEN`) for the served process; omit it and an existing env token is used, or a fresh one is minted and printed once so you can hand it to the mesh cells. `--db PATH` points the gateway at a specific SQLite file (`MESH_DB_PATH`). Run without the extra and the verb prints a `pip install "swarph-cli[gateway]"` hint and exits.
|
|
108
82
|
|
|
83
|
+
### `swarph service`
|
|
84
|
+
|
|
85
|
+
Turn a *subscription* LLM CLI into a **$0 HTTP lane** the mesh can call — `swarph service serve --provider <claude|codex|gemini>` runs a small FastAPI app exposing `POST /delegate` (bearer-authed) + `GET /health`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
$ pip install "swarph-cli[service]"
|
|
89
|
+
$ swarph service serve --provider claude # 127.0.0.1:8799, mints+prints a token
|
|
90
|
+
$ swarph service serve --provider gemini --host 100.x.y.z --port 8799 # expose on a tailnet IP
|
|
91
|
+
$ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
|
|
92
|
+
-d '{"prompt":"summarise this in one line: ..."}' # -> {"text": "...", "cost_usd": 0.0}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The load-bearing piece is **billing-scrub**: the subprocess env has every known provider API-key var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, …) stripped, so the wrapped CLI can *only* use its $0 subscription auth — it can never silently fall back to metered API billing. Default loopback bind (expose a tailnet IP explicitly); the bearer is `--token` > `SWARPH_SERVICE_TOKEN` > a freshly minted+printed token. Note the `gemini` (agy) lane reads the prompt from argv only and hard-caps its length (~4 KB) — very long prompts are rejected, not truncated. Run without the extra and the verb prints a `pip install "swarph-cli[service]"` hint and exits.
|
|
96
|
+
|
|
109
97
|
### `swarph highlight`
|
|
110
98
|
|
|
111
99
|
The swarph **timeline** is an append-only, multi-author `TIMELINE.md` — a git-backed continuous-learning log where every cell's highlights converge into one file. `swarph highlight` is the mechanics; *you* (or your agent) decide what's worth logging and which memory it links to.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.18.0"
|
|
8
8
|
description = "The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -70,6 +70,15 @@ gateway = [
|
|
|
70
70
|
"pydantic>=2.9",
|
|
71
71
|
"croniter>=6.2",
|
|
72
72
|
]
|
|
73
|
+
# `swarph service serve` stands up a $0 subscription-LLM HTTP lane. Same
|
|
74
|
+
# FastAPI/uvicorn stack as the gateway, behind its own extra so the core
|
|
75
|
+
# client paths stay dependency-light; the verb prints a
|
|
76
|
+
# `pip install swarph-cli[service]` hint when these are absent.
|
|
77
|
+
service = [
|
|
78
|
+
"fastapi>=0.115",
|
|
79
|
+
"uvicorn[standard]>=0.32",
|
|
80
|
+
"pydantic>=2.9",
|
|
81
|
+
]
|
|
73
82
|
|
|
74
83
|
[project.scripts]
|
|
75
84
|
swarph = "swarph_cli.main:main"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""``swarph brain`` — run the gbrain HTTP brain server (the $0 semantic memory).
|
|
2
|
+
|
|
3
|
+
``brain-ask`` *searches* a running brain; ``brain serve`` *runs* one. gbrain
|
|
4
|
+
(the sovereign $0 semantic-memory server) is an external binary — this verb is a
|
|
5
|
+
thin launcher that applies the swarph-blessed defaults: loopback bind (expose a
|
|
6
|
+
tailnet IP explicitly), and a 1-year token TTL (a short TTL silently 401s
|
|
7
|
+
long-lived mesh cells — a lesson learned the hard way). It replaces the current
|
|
8
|
+
process with ``gbrain serve`` via ``os.execvp`` so signals/stdio pass straight
|
|
9
|
+
through.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
swarph brain serve [--port 8792] [--bind 127.0.0.1]
|
|
13
|
+
[--token-ttl 31536000] [--gbrain-bin PATH]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
_DEFAULT_PORT = 8792
|
|
24
|
+
_DEFAULT_BIND = "127.0.0.1"
|
|
25
|
+
_DEFAULT_TOKEN_TTL = 31536000 # 1 year — short TTLs silently 401 long-lived cells
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _find_gbrain(explicit) -> str | None:
|
|
29
|
+
"""Resolve the gbrain binary: --gbrain-bin, else GBRAIN_BIN, else PATH."""
|
|
30
|
+
if explicit:
|
|
31
|
+
return explicit if (os.path.isfile(explicit) or shutil.which(explicit)) else None
|
|
32
|
+
env = os.environ.get("GBRAIN_BIN")
|
|
33
|
+
if env and (os.path.isfile(env) or shutil.which(env)):
|
|
34
|
+
return env
|
|
35
|
+
return shutil.which("gbrain")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_brain(argv: list) -> int:
|
|
39
|
+
p = argparse.ArgumentParser(
|
|
40
|
+
prog="swarph brain",
|
|
41
|
+
description="Run the gbrain HTTP brain server (the $0 semantic memory).")
|
|
42
|
+
sub = p.add_subparsers(dest="cmd")
|
|
43
|
+
serve = sub.add_parser("serve", help="run the gbrain HTTP brain server")
|
|
44
|
+
serve.add_argument("--port", type=int, default=_DEFAULT_PORT,
|
|
45
|
+
help=f"HTTP port (default {_DEFAULT_PORT})")
|
|
46
|
+
serve.add_argument("--bind", default=_DEFAULT_BIND,
|
|
47
|
+
help=f"bind address (default {_DEFAULT_BIND}; pass a tailnet "
|
|
48
|
+
"IP to expose on the mesh)")
|
|
49
|
+
serve.add_argument("--token-ttl", type=int, default=_DEFAULT_TOKEN_TTL,
|
|
50
|
+
help="minted-token lifetime in seconds (default 1 year — "
|
|
51
|
+
"short TTLs silently 401 long-lived mesh cells)")
|
|
52
|
+
serve.add_argument("--gbrain-bin", default=None,
|
|
53
|
+
help="path to the gbrain binary (else GBRAIN_BIN / PATH)")
|
|
54
|
+
args = p.parse_args(argv)
|
|
55
|
+
|
|
56
|
+
if args.cmd != "serve":
|
|
57
|
+
p.print_help()
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
gbin = _find_gbrain(args.gbrain_bin)
|
|
61
|
+
if not gbin:
|
|
62
|
+
print("swarph brain serve needs the `gbrain` binary on PATH (the sovereign "
|
|
63
|
+
"$0 semantic-memory server). Install it, or pass --gbrain-bin PATH / "
|
|
64
|
+
"set GBRAIN_BIN. `swarph brain-ask` queries an already-running brain.",
|
|
65
|
+
file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
|
|
68
|
+
cmd = [gbin, "serve", "--http",
|
|
69
|
+
"--port", str(args.port),
|
|
70
|
+
"--bind", args.bind,
|
|
71
|
+
"--token-ttl", str(args.token_ttl)]
|
|
72
|
+
# Replace this process with gbrain so it owns the terminal / signals.
|
|
73
|
+
os.execvp(gbin, cmd)
|
|
74
|
+
return 0 # unreachable when execvp succeeds; keeps the type-checker happy
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""``swarph service`` — stand up a $0 subscription-LLM HTTP lane.
|
|
2
|
+
|
|
3
|
+
`swarph service serve --provider <claude|codex|gemini>` runs a small FastAPI app
|
|
4
|
+
that exposes the provider's *subscription* CLI as a $0 HTTP lane (`POST /delegate`,
|
|
5
|
+
`GET /health`, bearer auth). The subprocess env is billing-scrubbed so the lane
|
|
6
|
+
can only use the $0 subscription path, never metered API billing. The server
|
|
7
|
+
stack (fastapi/uvicorn) is the optional ``[service]`` extra; the verb prints an
|
|
8
|
+
install hint and exits 2 when it's absent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import secrets
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _have_server_deps() -> bool:
|
|
20
|
+
try:
|
|
21
|
+
import fastapi # noqa: F401
|
|
22
|
+
import uvicorn # noqa: F401
|
|
23
|
+
return True
|
|
24
|
+
except ModuleNotFoundError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_service(argv: list) -> int:
|
|
29
|
+
p = argparse.ArgumentParser(
|
|
30
|
+
prog="swarph service",
|
|
31
|
+
description="Stand up a $0 subscription-LLM HTTP lane.")
|
|
32
|
+
sub = p.add_subparsers(dest="cmd")
|
|
33
|
+
serve = sub.add_parser("serve", help="run a $0 service lane for one provider")
|
|
34
|
+
serve.add_argument("--provider", required=True,
|
|
35
|
+
help="claude | codex | gemini")
|
|
36
|
+
serve.add_argument("--host", default="127.0.0.1",
|
|
37
|
+
help="bind host (default 127.0.0.1; pass a tailnet IP to expose)")
|
|
38
|
+
serve.add_argument("--port", type=int, default=8799, help="HTTP port (default 8799)")
|
|
39
|
+
serve.add_argument("--token", default=None,
|
|
40
|
+
help="bearer token (else SWARPH_SERVICE_TOKEN, else minted+printed)")
|
|
41
|
+
serve.add_argument("--timeout", type=int, default=120,
|
|
42
|
+
help="per-call subprocess timeout in seconds (default 120)")
|
|
43
|
+
args = p.parse_args(argv)
|
|
44
|
+
|
|
45
|
+
if args.cmd != "serve":
|
|
46
|
+
p.print_help()
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
from swarph_cli.service import providers as pv
|
|
50
|
+
if args.provider not in pv.SUPPORTED:
|
|
51
|
+
print(f"swarph service: unsupported provider {args.provider!r}; "
|
|
52
|
+
f"supported: {', '.join(pv.SUPPORTED)}", file=sys.stderr)
|
|
53
|
+
return 2
|
|
54
|
+
|
|
55
|
+
if not _have_server_deps():
|
|
56
|
+
print('swarph service serve needs the server extra: '
|
|
57
|
+
'pip install "swarph-cli[service]"', file=sys.stderr)
|
|
58
|
+
return 2
|
|
59
|
+
|
|
60
|
+
token = args.token or os.environ.get("SWARPH_SERVICE_TOKEN")
|
|
61
|
+
if not token:
|
|
62
|
+
token = secrets.token_urlsafe(32)
|
|
63
|
+
print("swarph service: minted bearer token (set SWARPH_SERVICE_TOKEN to "
|
|
64
|
+
f"reuse it):\n {token}", file=sys.stderr)
|
|
65
|
+
|
|
66
|
+
from swarph_cli.service.app import build_app
|
|
67
|
+
import uvicorn
|
|
68
|
+
|
|
69
|
+
app = build_app(args.provider, token, timeout=args.timeout)
|
|
70
|
+
print(f"swarph service: {args.provider} $0 lane on "
|
|
71
|
+
f"http://{args.host}:{args.port} (POST /delegate, GET /health)",
|
|
72
|
+
file=sys.stderr)
|
|
73
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
74
|
+
return 0
|
|
@@ -92,7 +92,9 @@ _VERB_HANDLERS: dict[str, str] = {
|
|
|
92
92
|
"compress": "swarph_cli.commands.compress.run_compress",
|
|
93
93
|
"cell": "swarph_cli.commands.cell.run_cell",
|
|
94
94
|
"brain-ask": "swarph_cli.commands.brain_ask.run_brain_ask",
|
|
95
|
+
"brain": "swarph_cli.commands.brain.run_brain",
|
|
95
96
|
"gateway": "swarph_cli.commands.gateway.run_gateway",
|
|
97
|
+
"service": "swarph_cli.commands.service.run_service",
|
|
96
98
|
"highlight": "swarph_cli.commands.highlight.run_highlight",
|
|
97
99
|
# Future: "list-peers", "list-adapters", etc.
|
|
98
100
|
}
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""FastAPI $0-service app — a subscription LLM CLI exposed as a $0 HTTP lane.
|
|
2
|
+
|
|
3
|
+
`POST /delegate` (bearer-authed) runs the provider's subscription CLI one-shot
|
|
4
|
+
with a billing-scrubbed env (see ``providers.scrub_billing_env``) and returns the
|
|
5
|
+
completion at ``cost_usd: 0.0``. Behind the optional ``[service]`` extra.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI, Header, HTTPException
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from . import providers as pv
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DelegateRequest(BaseModel):
|
|
19
|
+
prompt: str
|
|
20
|
+
system: str | None = None
|
|
21
|
+
timeout: int | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_app(provider: str, token: str, timeout: int = pv._DEFAULT_TIMEOUT) -> FastAPI:
|
|
25
|
+
if provider not in pv.SUPPORTED:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"unsupported provider {provider!r}; supported: {', '.join(pv.SUPPORTED)}")
|
|
28
|
+
|
|
29
|
+
app = FastAPI(title=f"swarph service ({provider})",
|
|
30
|
+
description="$0 subscription-LLM HTTP lane")
|
|
31
|
+
|
|
32
|
+
def _auth(authorization: str) -> None:
|
|
33
|
+
if authorization != f"Bearer {token}":
|
|
34
|
+
raise HTTPException(status_code=401, detail="bad or missing bearer token")
|
|
35
|
+
|
|
36
|
+
@app.get("/health")
|
|
37
|
+
def health():
|
|
38
|
+
return {"ok": True, "provider": provider, "cost_model": "$0 subscription"}
|
|
39
|
+
|
|
40
|
+
@app.post("/delegate")
|
|
41
|
+
def delegate(req: DelegateRequest, authorization: str = Header(default="")):
|
|
42
|
+
_auth(authorization)
|
|
43
|
+
prompt = req.prompt if not req.system else f"System: {req.system}\n\n{req.prompt}"
|
|
44
|
+
t0 = time.monotonic()
|
|
45
|
+
try:
|
|
46
|
+
text = pv.run_provider(provider, prompt, timeout=req.timeout or timeout)
|
|
47
|
+
except RuntimeError as exc:
|
|
48
|
+
# CLI failed — surface the (already-truncated, env-free) reason, never the env.
|
|
49
|
+
raise HTTPException(status_code=502, detail=str(exc))
|
|
50
|
+
return {"text": text, "provider": provider, "cost_usd": 0.0,
|
|
51
|
+
"duration_s": round(time.monotonic() - t0, 3)}
|
|
52
|
+
|
|
53
|
+
return app
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""$0-service provider core — billing-scrub + the CLI command map.
|
|
2
|
+
|
|
3
|
+
A swarph **service** wraps a *subscription* LLM CLI as a $0 HTTP lane. The
|
|
4
|
+
load-bearing security property: such a lane must NEVER be able to fall back to
|
|
5
|
+
metered API billing (the €50-Google-alert lesson, made structural). A
|
|
6
|
+
subscription CLI authenticates from its own config files (``~/.claude``,
|
|
7
|
+
``~/.codex``, ``~/.gemini/antigravity``, …) — *not* from an API-key env var. So
|
|
8
|
+
we strip every known provider API-key var from the subprocess env: if a stray
|
|
9
|
+
``ANTHROPIC_API_KEY`` were present, the CLI could silently bill it instead of
|
|
10
|
+
the subscription. Scrubbing makes the $0 path the only path.
|
|
11
|
+
|
|
12
|
+
No fastapi here — this module is the pure, importable security logic; the HTTP
|
|
13
|
+
app lives in ``service/app.py`` behind the optional ``[service]`` extra.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import subprocess
|
|
19
|
+
|
|
20
|
+
# Union of metered API-key env vars, stripped for EVERY lane (a claude lane must
|
|
21
|
+
# not be able to bill OpenAI either). These are unambiguously "use a paid API
|
|
22
|
+
# key" — subscription auth never lives here, so stripping them only removes the
|
|
23
|
+
# metered-fallback path.
|
|
24
|
+
_SCRUB_KEYS = frozenset({
|
|
25
|
+
"ANTHROPIC_API_KEY",
|
|
26
|
+
"OPENAI_API_KEY",
|
|
27
|
+
"GEMINI_API_KEY",
|
|
28
|
+
"GOOGLE_API_KEY",
|
|
29
|
+
"XAI_API_KEY",
|
|
30
|
+
"GROK_API_KEY",
|
|
31
|
+
"DEEPSEEK_API_KEY",
|
|
32
|
+
"MISTRAL_API_KEY",
|
|
33
|
+
"GROQ_API_KEY",
|
|
34
|
+
"TOGETHER_API_KEY",
|
|
35
|
+
"PERPLEXITY_API_KEY",
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
# provider -> argv prefix (the prompt is appended as the final arg). The prompt
|
|
39
|
+
# already carries any system context (the app composes it) so these stay simple.
|
|
40
|
+
# NOTE: the gemini lane (agy) reads the prompt ONLY from argv and hard-caps it
|
|
41
|
+
# (MAX_ARG_LEN ~4128) — very long prompts will be rejected by agy, not truncated.
|
|
42
|
+
_COMMANDS = {
|
|
43
|
+
"claude": ["claude", "-p"],
|
|
44
|
+
"codex": ["codex", "exec"],
|
|
45
|
+
"gemini": ["agy", "--print"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
SUPPORTED = tuple(_COMMANDS.keys())
|
|
49
|
+
|
|
50
|
+
_DEFAULT_TIMEOUT = 120 # seconds; mirrors CLAUDE_SUBSCRIPTION_TIMEOUT default
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def scrub_billing_env(provider: str, base_env: dict) -> dict:
|
|
54
|
+
"""Return a COPY of ``base_env`` with every metered API-key var removed.
|
|
55
|
+
|
|
56
|
+
``provider`` is accepted for symmetry / future per-provider nuance; today the
|
|
57
|
+
scrub is the full union for defense in depth. The input dict is never mutated.
|
|
58
|
+
"""
|
|
59
|
+
return {k: v for k, v in base_env.items() if k not in _SCRUB_KEYS}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def provider_command(provider: str, prompt: str) -> list:
|
|
63
|
+
"""Build the subprocess argv for ``provider`` running ``prompt`` one-shot."""
|
|
64
|
+
prefix = _COMMANDS.get(provider)
|
|
65
|
+
if prefix is None:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"unsupported provider {provider!r}; supported: {', '.join(SUPPORTED)}")
|
|
68
|
+
return [*prefix, prompt]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_provider(provider: str, prompt: str, timeout: int = _DEFAULT_TIMEOUT,
|
|
72
|
+
base_env: dict | None = None) -> str:
|
|
73
|
+
"""Run the provider's subscription CLI one-shot with a billing-scrubbed env.
|
|
74
|
+
|
|
75
|
+
Returns stdout (stripped). Raises ``ValueError`` for an unknown provider and
|
|
76
|
+
``RuntimeError`` on a non-zero exit (stderr head only — never echo the env).
|
|
77
|
+
"""
|
|
78
|
+
import os
|
|
79
|
+
|
|
80
|
+
argv = provider_command(provider, prompt)
|
|
81
|
+
env = scrub_billing_env(provider, os.environ if base_env is None else base_env)
|
|
82
|
+
proc = subprocess.run(argv, env=env, capture_output=True, text=True,
|
|
83
|
+
timeout=timeout)
|
|
84
|
+
if proc.returncode != 0:
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
f"{provider} CLI exited {proc.returncode}: {proc.stderr.strip()[:200]}")
|
|
87
|
+
return proc.stdout.strip()
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swarph-cli
|
|
3
|
+
Version: 0.18.0
|
|
4
|
+
Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
|
|
5
|
+
Author: Pierre Samson, Claude Opus
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BrainSurfing-tech/swarph-cli
|
|
8
|
+
Project-URL: Source, https://github.com/BrainSurfing-tech/swarph-cli
|
|
9
|
+
Project-URL: Substrate, https://github.com/BrainSurfing-tech/swarph-mesh
|
|
10
|
+
Project-URL: Spec, https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md
|
|
11
|
+
Keywords: swarph,llm,cli,mesh,gemini,claude,deepseek
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: swarph-mesh>=0.5.0
|
|
28
|
+
Requires-Dist: swarph-shared>=0.3.3
|
|
29
|
+
Requires-Dist: PyYAML>=6.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Provides-Extra: mcp
|
|
33
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
34
|
+
Provides-Extra: gateway
|
|
35
|
+
Requires-Dist: fastapi>=0.115; extra == "gateway"
|
|
36
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == "gateway"
|
|
37
|
+
Requires-Dist: pydantic>=2.9; extra == "gateway"
|
|
38
|
+
Requires-Dist: croniter>=6.2; extra == "gateway"
|
|
39
|
+
Provides-Extra: service
|
|
40
|
+
Requires-Dist: fastapi>=0.115; extra == "service"
|
|
41
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == "service"
|
|
42
|
+
Requires-Dist: pydantic>=2.9; extra == "service"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
1
45
|
# swarph-cli
|
|
2
46
|
|
|
3
47
|
**One CLI for every LLM.** Drive Claude, GPT, Gemini, DeepSeek, and Grok from a single binary — then connect them into a coordinating *mesh* where agents from different vendors talk to each other through a shared protocol, each staying itself.
|
|
@@ -25,7 +69,9 @@ This is one of three repos in the v0.3.x architecture:
|
|
|
25
69
|
swarph "prompt" one-shot against any provider (claude/openai/gemini/deepseek/grok)
|
|
26
70
|
swarph chat interactive REPL — multi-turn, slash commands, live provider switch
|
|
27
71
|
swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited answer or raw chunks
|
|
72
|
+
swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
|
|
28
73
|
swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
|
|
74
|
+
swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
|
|
29
75
|
swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
|
|
30
76
|
swarph spawn <role> launch a long-lived agent session as a named mesh cell
|
|
31
77
|
swarph daemon foreground inbox-drain loop (the mesh doorbell)
|
|
@@ -52,6 +98,18 @@ $ swarph brain-ask --no-synth --limit 3 "cross-vendor fallback governor order"
|
|
|
52
98
|
|
|
53
99
|
Config is via env, mirroring `swarph mesh`'s token model: `GBRAIN_MCP_URL` or `SWARPH_BRAIN_MCP` (gbrain endpoint; defaults to `http://127.0.0.1:8792/mcp`), and the read token resolved `--token-file` > `GBRAIN_TOKEN` > `SWARPH_BRAIN_TOKEN` > the mesh per-peer token (`~/.config/swarph/<self>.peer_token`) — so the mesh peer token doubles as the gbrain read token. Optional `SWARPH_FACADE` / `SWARPH_FACADE_TOKEN` enable the $0 cited-synthesis pass; without them, brain-ask prints the raw ranked chunks.
|
|
54
100
|
|
|
101
|
+
### `swarph brain serve`
|
|
102
|
+
|
|
103
|
+
`brain-ask` *searches* a running brain; **`brain serve`** *runs* one. gbrain (the sovereign $0 semantic-memory server) is an external binary — this verb is a thin launcher that applies the swarph-blessed defaults and replaces itself with `gbrain serve`:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
$ swarph brain serve # 127.0.0.1:8792, 1-year token TTL
|
|
107
|
+
$ swarph brain serve --bind 100.x.y.z --port 8792 # expose on a tailnet IP
|
|
108
|
+
$ swarph brain serve --gbrain-bin /opt/gbrain # explicit binary (else GBRAIN_BIN / PATH)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Defaults are loopback bind (expose a tailnet IP explicitly) and a **1-year token TTL** — a short TTL silently 401s long-lived mesh cells, a lesson learned the hard way. If `gbrain` isn't on `PATH` the verb prints an install hint and exits `2`; `brain-ask` still queries any already-running brain.
|
|
112
|
+
|
|
55
113
|
### `swarph gateway`
|
|
56
114
|
|
|
57
115
|
The **mesh-gateway** is the coordination/DM server every other verb talks to — the peer registry, the DM inbox/outbox, feature aggregation with allowlist + caps, and lane/service control. It used to be a separate deployment; it's now bundled, so any host can stand one up with a single command. It pairs with the client verbs (`mesh`, `spawn`, `daemon`, `watchdog`) and with `brain-ask`/gbrain to form the mesh's coordination plane.
|
|
@@ -66,6 +124,20 @@ $ swarph gateway serve --host 100.x.y.z --port 8788 # expose on a tailnet IP
|
|
|
66
124
|
|
|
67
125
|
`--token` sets the bearer (`MESH_GATEWAY_TOKEN`) for the served process; omit it and an existing env token is used, or a fresh one is minted and printed once so you can hand it to the mesh cells. `--db PATH` points the gateway at a specific SQLite file (`MESH_DB_PATH`). Run without the extra and the verb prints a `pip install "swarph-cli[gateway]"` hint and exits.
|
|
68
126
|
|
|
127
|
+
### `swarph service`
|
|
128
|
+
|
|
129
|
+
Turn a *subscription* LLM CLI into a **$0 HTTP lane** the mesh can call — `swarph service serve --provider <claude|codex|gemini>` runs a small FastAPI app exposing `POST /delegate` (bearer-authed) + `GET /health`:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
$ pip install "swarph-cli[service]"
|
|
133
|
+
$ swarph service serve --provider claude # 127.0.0.1:8799, mints+prints a token
|
|
134
|
+
$ swarph service serve --provider gemini --host 100.x.y.z --port 8799 # expose on a tailnet IP
|
|
135
|
+
$ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
|
|
136
|
+
-d '{"prompt":"summarise this in one line: ..."}' # -> {"text": "...", "cost_usd": 0.0}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The load-bearing piece is **billing-scrub**: the subprocess env has every known provider API-key var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, …) stripped, so the wrapped CLI can *only* use its $0 subscription auth — it can never silently fall back to metered API billing. Default loopback bind (expose a tailnet IP explicitly); the bearer is `--token` > `SWARPH_SERVICE_TOKEN` > a freshly minted+printed token. Note the `gemini` (agy) lane reads the prompt from argv only and hard-caps its length (~4 KB) — very long prompts are rejected, not truncated. Run without the extra and the verb prints a `pip install "swarph-cli[service]"` hint and exits.
|
|
140
|
+
|
|
69
141
|
### `swarph highlight`
|
|
70
142
|
|
|
71
143
|
The swarph **timeline** is an append-only, multi-author `TIMELINE.md` — a git-backed continuous-learning log where every cell's highlights converge into one file. `swarph highlight` is the mechanics; *you* (or your agent) decide what's worth logging and which memory it links to.
|
|
@@ -20,6 +20,7 @@ src/swarph_cli/capture/paths.py
|
|
|
20
20
|
src/swarph_cli/capture/verify.py
|
|
21
21
|
src/swarph_cli/commands/__init__.py
|
|
22
22
|
src/swarph_cli/commands/add.py
|
|
23
|
+
src/swarph_cli/commands/brain.py
|
|
23
24
|
src/swarph_cli/commands/brain_ask.py
|
|
24
25
|
src/swarph_cli/commands/cell.py
|
|
25
26
|
src/swarph_cli/commands/chat.py
|
|
@@ -39,6 +40,7 @@ src/swarph_cli/commands/onboard.py
|
|
|
39
40
|
src/swarph_cli/commands/protocol_handler.py
|
|
40
41
|
src/swarph_cli/commands/ratify.py
|
|
41
42
|
src/swarph_cli/commands/security.py
|
|
43
|
+
src/swarph_cli/commands/service.py
|
|
42
44
|
src/swarph_cli/commands/spawn.py
|
|
43
45
|
src/swarph_cli/commands/watchdog.py
|
|
44
46
|
src/swarph_cli/compress/__init__.py
|
|
@@ -53,6 +55,9 @@ src/swarph_cli/gateway/server.py
|
|
|
53
55
|
src/swarph_cli/gateway/services_control.py
|
|
54
56
|
src/swarph_cli/parsers/__init__.py
|
|
55
57
|
src/swarph_cli/parsers/claude.py
|
|
58
|
+
src/swarph_cli/service/__init__.py
|
|
59
|
+
src/swarph_cli/service/app.py
|
|
60
|
+
src/swarph_cli/service/providers.py
|
|
56
61
|
src/swarph_cli/systemd/swarph-watchdog.default
|
|
57
62
|
src/swarph_cli/systemd/swarph-watchdog.service
|
|
58
63
|
src/swarph_cli/systemd/swarph-watchdog.timer
|
|
@@ -65,6 +70,7 @@ tests/test_artifact_skill.py
|
|
|
65
70
|
tests/test_artifact_tool.py
|
|
66
71
|
tests/test_artifact_uri.py
|
|
67
72
|
tests/test_brain_ask_command.py
|
|
73
|
+
tests/test_brain_command.py
|
|
68
74
|
tests/test_caller_meta_guard.py
|
|
69
75
|
tests/test_capture_lineage.py
|
|
70
76
|
tests/test_capture_liveness.py
|
|
@@ -106,6 +112,9 @@ tests/test_protocol_handler.py
|
|
|
106
112
|
tests/test_ratify_command.py
|
|
107
113
|
tests/test_repl_caller_convention.py
|
|
108
114
|
tests/test_security.py
|
|
115
|
+
tests/test_service_app.py
|
|
116
|
+
tests/test_service_command.py
|
|
117
|
+
tests/test_service_providers.py
|
|
109
118
|
tests/test_session_path_role_gate.py
|
|
110
119
|
tests/test_smoke_chat.py
|
|
111
120
|
tests/test_smoke_one_shot.py
|