swarph-cli 0.18.0__tar.gz → 0.19.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.18.0/src/swarph_cli.egg-info → swarph_cli-0.19.0}/PKG-INFO +29 -1
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/README.md +28 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/pyproject.toml +1 -1
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.19.0/src/swarph_cli/commands/_gateway_client.py +38 -0
- swarph_cli-0.19.0/src/swarph_cli/commands/channel.py +154 -0
- swarph_cli-0.19.0/src/swarph_cli/commands/lane.py +175 -0
- swarph_cli-0.19.0/src/swarph_cli/commands/schedule.py +185 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/main.py +3 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0/src/swarph_cli.egg-info}/PKG-INFO +29 -1
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/SOURCES.txt +8 -0
- swarph_cli-0.19.0/tests/test_channel_command.py +141 -0
- swarph_cli-0.19.0/tests/test_gateway_client.py +52 -0
- swarph_cli-0.19.0/tests/test_lane_command.py +101 -0
- swarph_cli-0.19.0/tests/test_schedule_command.py +163 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/LICENSE +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/setup.cfg +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_brain_ask_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_init_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_main.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_security.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_service_app.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_service_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.18.0 → swarph_cli-0.19.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.19.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
|
|
@@ -72,6 +72,9 @@ swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited an
|
|
|
72
72
|
swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
|
|
73
73
|
swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
|
|
74
74
|
swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
|
|
75
|
+
swarph channel <sub> mesh channels — create/join/leave/list/members
|
|
76
|
+
swarph schedule <sub> scheduled events — create/list/get/enable/disable/delete/fire-now
|
|
77
|
+
swarph lane <sub> $0-lane orchestration — list/create/scale/delete/enqueue
|
|
75
78
|
swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
|
|
76
79
|
swarph spawn <role> launch a long-lived agent session as a named mesh cell
|
|
77
80
|
swarph daemon foreground inbox-drain loop (the mesh doorbell)
|
|
@@ -138,6 +141,31 @@ $ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
|
|
|
138
141
|
|
|
139
142
|
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
143
|
|
|
144
|
+
### `swarph channel` / `swarph schedule` / `swarph lane`
|
|
145
|
+
|
|
146
|
+
The gateway's **automation control plane** — channels (pub/sub), scheduled events, and the $0-lane orchestration — as first-class client verbs. They share `mesh`'s auth: identity is `--as` / `SWARPH_SELF`, the bearer is `--token-file` / `MESH_GATEWAY_TOKEN` / the peer-token file, and `--gateway` (default `http://localhost:8788`) points at the hub.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# channels — converge work into pub/sub rooms
|
|
150
|
+
$ swarph channel create research --kind topic --description "market-structure notes"
|
|
151
|
+
$ swarph channel join research --wake-policy mentions_only
|
|
152
|
+
$ swarph channel list
|
|
153
|
+
|
|
154
|
+
# scheduled events — operator-gated recurring/ conditional fires (a 403 is surfaced verbatim)
|
|
155
|
+
$ swarph schedule create nightly-digest --trigger time --cron "0 7 * * *" \
|
|
156
|
+
--target lab-ovh --task "compile the overnight digest" --context "[[some-anchor]]"
|
|
157
|
+
$ swarph schedule enable nightly-digest
|
|
158
|
+
$ swarph schedule fire-now nightly-digest
|
|
159
|
+
|
|
160
|
+
# lanes — drive the gateway's $0 worker pools
|
|
161
|
+
$ swarph lane list
|
|
162
|
+
$ swarph lane create judges --provider claude --model sonnet --n 3
|
|
163
|
+
$ swarph lane enqueue judges --prompt "score this design ..."
|
|
164
|
+
$ swarph lane scale judges --n 0
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Pure-stdlib clients (no extra needed). Mutating channel/schedule/lane operations are operator-gated *server-side* — the verb just shapes and sends the request; an unauthorized caller sees the gateway's `403` on the normal error path.
|
|
168
|
+
|
|
141
169
|
### `swarph highlight`
|
|
142
170
|
|
|
143
171
|
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.
|
|
@@ -28,6 +28,9 @@ swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited an
|
|
|
28
28
|
swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
|
|
29
29
|
swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
|
|
30
30
|
swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
|
|
31
|
+
swarph channel <sub> mesh channels — create/join/leave/list/members
|
|
32
|
+
swarph schedule <sub> scheduled events — create/list/get/enable/disable/delete/fire-now
|
|
33
|
+
swarph lane <sub> $0-lane orchestration — list/create/scale/delete/enqueue
|
|
31
34
|
swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
|
|
32
35
|
swarph spawn <role> launch a long-lived agent session as a named mesh cell
|
|
33
36
|
swarph daemon foreground inbox-drain loop (the mesh doorbell)
|
|
@@ -94,6 +97,31 @@ $ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
|
|
|
94
97
|
|
|
95
98
|
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
99
|
|
|
100
|
+
### `swarph channel` / `swarph schedule` / `swarph lane`
|
|
101
|
+
|
|
102
|
+
The gateway's **automation control plane** — channels (pub/sub), scheduled events, and the $0-lane orchestration — as first-class client verbs. They share `mesh`'s auth: identity is `--as` / `SWARPH_SELF`, the bearer is `--token-file` / `MESH_GATEWAY_TOKEN` / the peer-token file, and `--gateway` (default `http://localhost:8788`) points at the hub.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# channels — converge work into pub/sub rooms
|
|
106
|
+
$ swarph channel create research --kind topic --description "market-structure notes"
|
|
107
|
+
$ swarph channel join research --wake-policy mentions_only
|
|
108
|
+
$ swarph channel list
|
|
109
|
+
|
|
110
|
+
# scheduled events — operator-gated recurring/ conditional fires (a 403 is surfaced verbatim)
|
|
111
|
+
$ swarph schedule create nightly-digest --trigger time --cron "0 7 * * *" \
|
|
112
|
+
--target lab-ovh --task "compile the overnight digest" --context "[[some-anchor]]"
|
|
113
|
+
$ swarph schedule enable nightly-digest
|
|
114
|
+
$ swarph schedule fire-now nightly-digest
|
|
115
|
+
|
|
116
|
+
# lanes — drive the gateway's $0 worker pools
|
|
117
|
+
$ swarph lane list
|
|
118
|
+
$ swarph lane create judges --provider claude --model sonnet --n 3
|
|
119
|
+
$ swarph lane enqueue judges --prompt "score this design ..."
|
|
120
|
+
$ swarph lane scale judges --n 0
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Pure-stdlib clients (no extra needed). Mutating channel/schedule/lane operations are operator-gated *server-side* — the verb just shapes and sends the request; an unauthorized caller sees the gateway's `403` on the normal error path.
|
|
124
|
+
|
|
97
125
|
### `swarph highlight`
|
|
98
126
|
|
|
99
127
|
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.19.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" }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Shared mesh-gateway HTTP-client helpers for the control-plane client verbs
|
|
2
|
+
(``channel`` / ``schedule`` / ``lane``).
|
|
3
|
+
|
|
4
|
+
Thin re-export of the ``mesh`` verb's proven auth + HTTP layer (so all gateway
|
|
5
|
+
clients resolve identity/token and shape requests identically), plus a DELETE
|
|
6
|
+
helper that ``mesh`` didn't need. Importing the underlying ``mesh`` helpers keeps
|
|
7
|
+
one battle-tested implementation rather than four divergent copies.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
|
|
16
|
+
from .mesh import _add_common as add_common_args # noqa: F401
|
|
17
|
+
from .mesh import _http_get_json as get_json # noqa: F401
|
|
18
|
+
from .mesh import _post_json as post_json # noqa: F401
|
|
19
|
+
from .mesh import _resolve_self_name as resolve_self_name # noqa: F401
|
|
20
|
+
from .mesh import _resolve_token as resolve_token # noqa: F401
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def delete_json(url: str, token: str, *, timeout: float = 10.0):
|
|
24
|
+
"""DELETE ``url`` with a bearer token, returning ``(status, body)`` like the
|
|
25
|
+
mesh GET/POST helpers (status 0 on a connection-level URLError)."""
|
|
26
|
+
req = urllib.request.Request(
|
|
27
|
+
url, method="DELETE", headers={"Authorization": f"Bearer {token}"})
|
|
28
|
+
try:
|
|
29
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
30
|
+
return resp.status, json.loads(resp.read().decode("utf-8") or "{}")
|
|
31
|
+
except urllib.error.HTTPError as exc:
|
|
32
|
+
try:
|
|
33
|
+
err_body = json.loads(exc.read().decode("utf-8") or "{}")
|
|
34
|
+
except Exception:
|
|
35
|
+
err_body = {"detail": str(exc)}
|
|
36
|
+
return exc.code, err_body
|
|
37
|
+
except urllib.error.URLError as exc:
|
|
38
|
+
return 0, {"detail": str(exc)}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""``swarph channel`` — mesh CHANNELS control-plane client (create / list /
|
|
2
|
+
join / leave / members) over the shared gateway HTTP layer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import urllib.parse
|
|
10
|
+
|
|
11
|
+
from ._gateway_client import (
|
|
12
|
+
add_common_args,
|
|
13
|
+
get_json,
|
|
14
|
+
post_json,
|
|
15
|
+
resolve_self_name,
|
|
16
|
+
resolve_token,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
p = argparse.ArgumentParser(
|
|
22
|
+
prog="swarph channel",
|
|
23
|
+
description="Mesh CHANNELS control-plane commands.",
|
|
24
|
+
)
|
|
25
|
+
sub = p.add_subparsers(dest="command")
|
|
26
|
+
|
|
27
|
+
create = sub.add_parser("create", help="create a channel")
|
|
28
|
+
create.add_argument("name", help="channel name")
|
|
29
|
+
create.add_argument(
|
|
30
|
+
"--kind", required=True, choices=["announce", "topic", "group"]
|
|
31
|
+
)
|
|
32
|
+
create.add_argument("--visibility", default=None, choices=["open", "invite"])
|
|
33
|
+
create.add_argument("--description", default=None)
|
|
34
|
+
add_common_args(create)
|
|
35
|
+
|
|
36
|
+
listp = sub.add_parser("list", help="list channels")
|
|
37
|
+
add_common_args(listp)
|
|
38
|
+
|
|
39
|
+
join = sub.add_parser("join", help="join a channel")
|
|
40
|
+
join.add_argument("name", help="channel name")
|
|
41
|
+
join.add_argument(
|
|
42
|
+
"--wake-policy",
|
|
43
|
+
default=None,
|
|
44
|
+
choices=["mentions_only", "here_and_mentions", "all", "muted"],
|
|
45
|
+
)
|
|
46
|
+
add_common_args(join)
|
|
47
|
+
|
|
48
|
+
leave = sub.add_parser("leave", help="leave a channel")
|
|
49
|
+
leave.add_argument("name", help="channel name")
|
|
50
|
+
add_common_args(leave)
|
|
51
|
+
|
|
52
|
+
members = sub.add_parser("members", help="list channel members")
|
|
53
|
+
members.add_argument("name", help="channel name")
|
|
54
|
+
add_common_args(members)
|
|
55
|
+
|
|
56
|
+
return p
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ctx(args: argparse.Namespace) -> tuple[str, str, str]:
|
|
60
|
+
self_name = resolve_self_name(args.self_name)
|
|
61
|
+
token = resolve_token(self_name, args.token_file)
|
|
62
|
+
return self_name, token, args.gateway.rstrip("/")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fail(sub: str, status: int, payload: dict) -> int:
|
|
66
|
+
detail = payload.get("detail", "<error>")
|
|
67
|
+
print(f"swarph channel {sub}: gateway {status}: {detail}", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _ok(status: int) -> bool:
|
|
72
|
+
return 200 <= status < 300
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _run_create(args: argparse.Namespace) -> int:
|
|
76
|
+
self_name, token, base = _ctx(args)
|
|
77
|
+
body = {"name": args.name, "kind": args.kind, "created_by": self_name}
|
|
78
|
+
if args.visibility is not None:
|
|
79
|
+
body["visibility"] = args.visibility
|
|
80
|
+
if args.description is not None:
|
|
81
|
+
body["description"] = args.description
|
|
82
|
+
status, payload = post_json(f"{base}/channels", body, token)
|
|
83
|
+
if not _ok(status):
|
|
84
|
+
return _fail("create", status, payload)
|
|
85
|
+
print(f"created channel {args.name}")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _run_list(args: argparse.Namespace) -> int:
|
|
90
|
+
_self, token, base = _ctx(args)
|
|
91
|
+
status, payload = get_json(f"{base}/channels", token)
|
|
92
|
+
if not _ok(status):
|
|
93
|
+
return _fail("list", status, payload)
|
|
94
|
+
print(json.dumps(payload, indent=2))
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run_join(args: argparse.Namespace) -> int:
|
|
99
|
+
self_name, token, base = _ctx(args)
|
|
100
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
101
|
+
body = {"peer": self_name}
|
|
102
|
+
if args.wake_policy is not None:
|
|
103
|
+
body["wake_policy"] = args.wake_policy
|
|
104
|
+
status, payload = post_json(f"{base}/channels/{name}/join", body, token)
|
|
105
|
+
if not _ok(status):
|
|
106
|
+
return _fail("join", status, payload)
|
|
107
|
+
print(f"joined {args.name}")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _run_leave(args: argparse.Namespace) -> int:
|
|
112
|
+
self_name, token, base = _ctx(args)
|
|
113
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
114
|
+
status, payload = post_json(
|
|
115
|
+
f"{base}/channels/{name}/leave", {"peer": self_name}, token
|
|
116
|
+
)
|
|
117
|
+
if not _ok(status):
|
|
118
|
+
return _fail("leave", status, payload)
|
|
119
|
+
print(f"left {args.name}")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _run_members(args: argparse.Namespace) -> int:
|
|
124
|
+
_self, token, base = _ctx(args)
|
|
125
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
126
|
+
status, payload = get_json(f"{base}/channels/{name}/members", token)
|
|
127
|
+
if not _ok(status):
|
|
128
|
+
return _fail("members", status, payload)
|
|
129
|
+
print(json.dumps(payload, indent=2))
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_channel(argv: list) -> int:
|
|
134
|
+
p = _build_parser()
|
|
135
|
+
args = p.parse_args(argv)
|
|
136
|
+
if args.command is None:
|
|
137
|
+
p.print_help()
|
|
138
|
+
return 0
|
|
139
|
+
try:
|
|
140
|
+
if args.command == "create":
|
|
141
|
+
return _run_create(args)
|
|
142
|
+
if args.command == "list":
|
|
143
|
+
return _run_list(args)
|
|
144
|
+
if args.command == "join":
|
|
145
|
+
return _run_join(args)
|
|
146
|
+
if args.command == "leave":
|
|
147
|
+
return _run_leave(args)
|
|
148
|
+
if args.command == "members":
|
|
149
|
+
return _run_members(args)
|
|
150
|
+
except RuntimeError as e:
|
|
151
|
+
print(f"swarph channel: {e}", file=sys.stderr)
|
|
152
|
+
return 2
|
|
153
|
+
p.print_help()
|
|
154
|
+
return 0
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""``swarph lane`` — client for the gateway's $0-lane orchestration.
|
|
2
|
+
|
|
3
|
+
A lane is a pool of provider-backed workers the gateway scales on demand;
|
|
4
|
+
``list``/``create``/``scale``/``delete``/``enqueue`` drive that control plane.
|
|
5
|
+
Create/scale/delete are operator-gated server-side, so a 403 surfaces through
|
|
6
|
+
the non-2xx path here like any other gateway error.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.parse
|
|
15
|
+
|
|
16
|
+
from ._gateway_client import (
|
|
17
|
+
add_common_args,
|
|
18
|
+
delete_json,
|
|
19
|
+
get_json,
|
|
20
|
+
post_json,
|
|
21
|
+
resolve_self_name,
|
|
22
|
+
resolve_token,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
p = argparse.ArgumentParser(
|
|
28
|
+
prog="swarph lane",
|
|
29
|
+
description="Drive the gateway's $0-lane orchestration.",
|
|
30
|
+
)
|
|
31
|
+
sub = p.add_subparsers(dest="command")
|
|
32
|
+
|
|
33
|
+
ls = sub.add_parser("list", help="list lanes")
|
|
34
|
+
add_common_args(ls)
|
|
35
|
+
|
|
36
|
+
create = sub.add_parser("create", help="create a lane (operator-gated)")
|
|
37
|
+
create.add_argument("name", help="lane name")
|
|
38
|
+
create.add_argument("--provider", required=True, help="lane provider")
|
|
39
|
+
create.add_argument("--model", required=True, help="provider model")
|
|
40
|
+
create.add_argument("--n", type=int, default=0, help="initial worker count")
|
|
41
|
+
create.add_argument("--context-text", default=None, help="inline context text")
|
|
42
|
+
create.add_argument(
|
|
43
|
+
"--context-file",
|
|
44
|
+
action="append",
|
|
45
|
+
default=[],
|
|
46
|
+
help="context file path (repeatable)",
|
|
47
|
+
)
|
|
48
|
+
add_common_args(create)
|
|
49
|
+
|
|
50
|
+
scale = sub.add_parser("scale", help="scale a lane (operator-gated)")
|
|
51
|
+
scale.add_argument("name", help="lane name")
|
|
52
|
+
scale.add_argument("--n", type=int, required=True, help="target worker count")
|
|
53
|
+
add_common_args(scale)
|
|
54
|
+
|
|
55
|
+
delete = sub.add_parser("delete", help="delete a lane (operator-gated)")
|
|
56
|
+
delete.add_argument("name", help="lane name")
|
|
57
|
+
add_common_args(delete)
|
|
58
|
+
|
|
59
|
+
enqueue = sub.add_parser("enqueue", help="enqueue a job on a lane")
|
|
60
|
+
enqueue.add_argument("name", help="lane name")
|
|
61
|
+
enqueue.add_argument("--prompt", required=True, help="job prompt")
|
|
62
|
+
enqueue.add_argument("--context-text", default=None, help="inline context text")
|
|
63
|
+
enqueue.add_argument(
|
|
64
|
+
"--context-file",
|
|
65
|
+
action="append",
|
|
66
|
+
default=[],
|
|
67
|
+
help="context file path (repeatable)",
|
|
68
|
+
)
|
|
69
|
+
add_common_args(enqueue)
|
|
70
|
+
|
|
71
|
+
return p
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _fail(sub: str, status: int, payload: dict) -> int:
|
|
75
|
+
detail = payload.get("detail", "<error>")
|
|
76
|
+
print(f"swarph lane {sub}: gateway {status}: {detail}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _ok(status: int) -> bool:
|
|
81
|
+
return 200 <= status < 300
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _auth(args: argparse.Namespace) -> tuple[str, str]:
|
|
85
|
+
self_name = resolve_self_name(args.self_name)
|
|
86
|
+
token = resolve_token(self_name, args.token_file)
|
|
87
|
+
return self_name, token
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _run_list(args: argparse.Namespace) -> int:
|
|
91
|
+
_, token = _auth(args)
|
|
92
|
+
base = args.gateway.rstrip("/")
|
|
93
|
+
status, payload = get_json(f"{base}/lanes", token)
|
|
94
|
+
if not _ok(status):
|
|
95
|
+
return _fail("list", status, payload)
|
|
96
|
+
print(json.dumps(payload, indent=2))
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run_create(args: argparse.Namespace) -> int:
|
|
101
|
+
_, token = _auth(args)
|
|
102
|
+
base = args.gateway.rstrip("/")
|
|
103
|
+
body = {
|
|
104
|
+
"name": args.name,
|
|
105
|
+
"provider": args.provider,
|
|
106
|
+
"model": args.model,
|
|
107
|
+
"n": args.n,
|
|
108
|
+
"context_text": args.context_text or "",
|
|
109
|
+
"context_files": args.context_file,
|
|
110
|
+
}
|
|
111
|
+
status, payload = post_json(f"{base}/lanes", body, token)
|
|
112
|
+
if not _ok(status):
|
|
113
|
+
return _fail("create", status, payload)
|
|
114
|
+
print(f"created lane {args.name}")
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run_scale(args: argparse.Namespace) -> int:
|
|
119
|
+
_, token = _auth(args)
|
|
120
|
+
base = args.gateway.rstrip("/")
|
|
121
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
122
|
+
status, payload = post_json(f"{base}/lanes/{name}/scale", {"n": args.n}, token)
|
|
123
|
+
if not _ok(status):
|
|
124
|
+
return _fail("scale", status, payload)
|
|
125
|
+
print(f"scaled {args.name} -> {args.n}")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _run_delete(args: argparse.Namespace) -> int:
|
|
130
|
+
_, token = _auth(args)
|
|
131
|
+
base = args.gateway.rstrip("/")
|
|
132
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
133
|
+
status, payload = delete_json(f"{base}/lanes/{name}", token)
|
|
134
|
+
if not _ok(status):
|
|
135
|
+
return _fail("delete", status, payload)
|
|
136
|
+
print(f"deleted lane {args.name}")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _run_enqueue(args: argparse.Namespace) -> int:
|
|
141
|
+
_, token = _auth(args)
|
|
142
|
+
base = args.gateway.rstrip("/")
|
|
143
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
144
|
+
body = {
|
|
145
|
+
"prompt": args.prompt,
|
|
146
|
+
"context_text": args.context_text or "",
|
|
147
|
+
"context_files": args.context_file,
|
|
148
|
+
}
|
|
149
|
+
status, payload = post_json(f"{base}/lanes/{name}/enqueue", body, token)
|
|
150
|
+
if not _ok(status):
|
|
151
|
+
return _fail("enqueue", status, payload)
|
|
152
|
+
print(f"enqueued on {args.name}: {json.dumps(payload)}")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_DISPATCH = {
|
|
157
|
+
"list": _run_list,
|
|
158
|
+
"create": _run_create,
|
|
159
|
+
"scale": _run_scale,
|
|
160
|
+
"delete": _run_delete,
|
|
161
|
+
"enqueue": _run_enqueue,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_lane(argv: list) -> int:
|
|
166
|
+
parser = _build_parser()
|
|
167
|
+
args = parser.parse_args(argv)
|
|
168
|
+
if args.command is None:
|
|
169
|
+
parser.print_help()
|
|
170
|
+
return 0
|
|
171
|
+
try:
|
|
172
|
+
return _DISPATCH[args.command](args)
|
|
173
|
+
except RuntimeError as exc:
|
|
174
|
+
print(f"swarph lane: {exc}", file=sys.stderr)
|
|
175
|
+
return 2
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""``swarph schedule`` — client for the mesh-gateway scheduled-event endpoints.
|
|
2
|
+
|
|
3
|
+
These endpoints are operator-gated server-side; this client only shapes and
|
|
4
|
+
sends the requests. A 403 (or any non-2xx) is surfaced verbatim from the
|
|
5
|
+
gateway, so an unauthorized caller sees the operator-only refusal.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import urllib.parse
|
|
14
|
+
|
|
15
|
+
from ._gateway_client import (
|
|
16
|
+
add_common_args,
|
|
17
|
+
delete_json,
|
|
18
|
+
get_json,
|
|
19
|
+
post_json,
|
|
20
|
+
resolve_self_name,
|
|
21
|
+
resolve_token,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
p = argparse.ArgumentParser(
|
|
27
|
+
prog="swarph schedule",
|
|
28
|
+
description="Manage mesh-gateway scheduled events.",
|
|
29
|
+
)
|
|
30
|
+
sub = p.add_subparsers(dest="command")
|
|
31
|
+
|
|
32
|
+
create = sub.add_parser("create", help="create a scheduled event")
|
|
33
|
+
create.add_argument("name", help="event name")
|
|
34
|
+
create.add_argument("--trigger", required=True, help="trigger type: time|event")
|
|
35
|
+
create.add_argument("--target", required=True, help="target cell")
|
|
36
|
+
create.add_argument("--task", required=True, help="task text")
|
|
37
|
+
create.add_argument("--cron", default=None, help="cron expression (time trigger)")
|
|
38
|
+
create.add_argument("--out-channel", default=None, help="output channel")
|
|
39
|
+
create.add_argument(
|
|
40
|
+
"--context",
|
|
41
|
+
action="append",
|
|
42
|
+
default=[],
|
|
43
|
+
help="context reference (repeatable)",
|
|
44
|
+
)
|
|
45
|
+
create.add_argument(
|
|
46
|
+
"--min-interval",
|
|
47
|
+
type=int,
|
|
48
|
+
default=None,
|
|
49
|
+
help="minimum seconds between firings",
|
|
50
|
+
)
|
|
51
|
+
add_common_args(create)
|
|
52
|
+
|
|
53
|
+
ls = sub.add_parser("list", help="list scheduled events")
|
|
54
|
+
add_common_args(ls)
|
|
55
|
+
|
|
56
|
+
get = sub.add_parser("get", help="show one scheduled event")
|
|
57
|
+
get.add_argument("name", help="event name")
|
|
58
|
+
add_common_args(get)
|
|
59
|
+
|
|
60
|
+
enable = sub.add_parser("enable", help="enable a scheduled event")
|
|
61
|
+
enable.add_argument("name", help="event name")
|
|
62
|
+
add_common_args(enable)
|
|
63
|
+
|
|
64
|
+
disable = sub.add_parser("disable", help="disable a scheduled event")
|
|
65
|
+
disable.add_argument("name", help="event name")
|
|
66
|
+
add_common_args(disable)
|
|
67
|
+
|
|
68
|
+
delete = sub.add_parser("delete", help="delete a scheduled event")
|
|
69
|
+
delete.add_argument("name", help="event name")
|
|
70
|
+
add_common_args(delete)
|
|
71
|
+
|
|
72
|
+
fire = sub.add_parser("fire-now", help="fire a scheduled event immediately")
|
|
73
|
+
fire.add_argument("name", help="event name")
|
|
74
|
+
add_common_args(fire)
|
|
75
|
+
|
|
76
|
+
return p
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _fail(sub: str, status: int, payload: dict) -> int:
|
|
80
|
+
detail = payload.get("detail", "<error>")
|
|
81
|
+
print(f"swarph schedule {sub}: gateway {status}: {detail}", file=sys.stderr)
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ok(status: int) -> bool:
|
|
86
|
+
return 200 <= status < 300
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _ctx(args: argparse.Namespace) -> tuple[str, str, str]:
|
|
90
|
+
self_name = resolve_self_name(args.self_name)
|
|
91
|
+
token = resolve_token(self_name, args.token_file)
|
|
92
|
+
base = args.gateway.rstrip("/")
|
|
93
|
+
return self_name, token, base
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_create(args: argparse.Namespace) -> int:
|
|
97
|
+
self_name, token, base = _ctx(args)
|
|
98
|
+
body = {
|
|
99
|
+
"name": args.name,
|
|
100
|
+
"trigger_type": args.trigger,
|
|
101
|
+
"target_cell": args.target,
|
|
102
|
+
"task": args.task,
|
|
103
|
+
"context_ref": args.context,
|
|
104
|
+
"created_by": self_name,
|
|
105
|
+
}
|
|
106
|
+
if args.cron is not None:
|
|
107
|
+
body["cron"] = args.cron
|
|
108
|
+
if args.out_channel is not None:
|
|
109
|
+
body["out_channel"] = args.out_channel
|
|
110
|
+
if args.min_interval is not None:
|
|
111
|
+
body["min_interval_sec"] = args.min_interval
|
|
112
|
+
status, payload = post_json(f"{base}/scheduled-events", body, token)
|
|
113
|
+
if not _ok(status):
|
|
114
|
+
return _fail("create", status, payload)
|
|
115
|
+
print(f"scheduled {args.name}")
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _run_list(args: argparse.Namespace) -> int:
|
|
120
|
+
_self, token, base = _ctx(args)
|
|
121
|
+
status, payload = get_json(f"{base}/scheduled-events", token)
|
|
122
|
+
if not _ok(status):
|
|
123
|
+
return _fail("list", status, payload)
|
|
124
|
+
print(json.dumps(payload, indent=2))
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run_get(args: argparse.Namespace) -> int:
|
|
129
|
+
_self, token, base = _ctx(args)
|
|
130
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
131
|
+
status, payload = get_json(f"{base}/scheduled-events/{name}", token)
|
|
132
|
+
if not _ok(status):
|
|
133
|
+
return _fail("get", status, payload)
|
|
134
|
+
print(json.dumps(payload, indent=2))
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _run_action(args: argparse.Namespace, sub: str, action: str, verb: str) -> int:
|
|
139
|
+
_self, token, base = _ctx(args)
|
|
140
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
141
|
+
status, payload = post_json(
|
|
142
|
+
f"{base}/scheduled-events/{name}/{action}", {}, token
|
|
143
|
+
)
|
|
144
|
+
if not _ok(status):
|
|
145
|
+
return _fail(sub, status, payload)
|
|
146
|
+
print(f"{verb} {args.name}")
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _run_delete(args: argparse.Namespace) -> int:
|
|
151
|
+
_self, token, base = _ctx(args)
|
|
152
|
+
name = urllib.parse.quote(args.name, safe="")
|
|
153
|
+
status, payload = delete_json(f"{base}/scheduled-events/{name}", token)
|
|
154
|
+
if not _ok(status):
|
|
155
|
+
return _fail("delete", status, payload)
|
|
156
|
+
print(f"deleted {args.name}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_schedule(argv: list) -> int:
|
|
161
|
+
parser = _build_parser()
|
|
162
|
+
args = parser.parse_args(argv)
|
|
163
|
+
if args.command is None:
|
|
164
|
+
parser.print_help()
|
|
165
|
+
return 0
|
|
166
|
+
try:
|
|
167
|
+
if args.command == "create":
|
|
168
|
+
return _run_create(args)
|
|
169
|
+
if args.command == "list":
|
|
170
|
+
return _run_list(args)
|
|
171
|
+
if args.command == "get":
|
|
172
|
+
return _run_get(args)
|
|
173
|
+
if args.command == "enable":
|
|
174
|
+
return _run_action(args, "enable", "enable", "enabled")
|
|
175
|
+
if args.command == "disable":
|
|
176
|
+
return _run_action(args, "disable", "disable", "disabled")
|
|
177
|
+
if args.command == "delete":
|
|
178
|
+
return _run_delete(args)
|
|
179
|
+
if args.command == "fire-now":
|
|
180
|
+
return _run_action(args, "fire-now", "fire-now", "fired")
|
|
181
|
+
parser.error(f"unknown command: {args.command}")
|
|
182
|
+
except RuntimeError as exc:
|
|
183
|
+
print(f"swarph schedule: {exc}", file=sys.stderr)
|
|
184
|
+
return 2
|
|
185
|
+
return 2
|
|
@@ -95,6 +95,9 @@ _VERB_HANDLERS: dict[str, str] = {
|
|
|
95
95
|
"brain": "swarph_cli.commands.brain.run_brain",
|
|
96
96
|
"gateway": "swarph_cli.commands.gateway.run_gateway",
|
|
97
97
|
"service": "swarph_cli.commands.service.run_service",
|
|
98
|
+
"channel": "swarph_cli.commands.channel.run_channel",
|
|
99
|
+
"schedule": "swarph_cli.commands.schedule.run_schedule",
|
|
100
|
+
"lane": "swarph_cli.commands.lane.run_lane",
|
|
98
101
|
"highlight": "swarph_cli.commands.highlight.run_highlight",
|
|
99
102
|
# Future: "list-peers", "list-adapters", etc.
|
|
100
103
|
}
|