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.
Files changed (143) hide show
  1. {swarph_cli-0.18.0/src/swarph_cli.egg-info → swarph_cli-0.19.0}/PKG-INFO +29 -1
  2. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/README.md +28 -0
  3. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/pyproject.toml +1 -1
  4. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/__init__.py +1 -1
  5. swarph_cli-0.19.0/src/swarph_cli/commands/_gateway_client.py +38 -0
  6. swarph_cli-0.19.0/src/swarph_cli/commands/channel.py +154 -0
  7. swarph_cli-0.19.0/src/swarph_cli/commands/lane.py +175 -0
  8. swarph_cli-0.19.0/src/swarph_cli/commands/schedule.py +185 -0
  9. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/main.py +3 -0
  10. {swarph_cli-0.18.0 → swarph_cli-0.19.0/src/swarph_cli.egg-info}/PKG-INFO +29 -1
  11. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/SOURCES.txt +8 -0
  12. swarph_cli-0.19.0/tests/test_channel_command.py +141 -0
  13. swarph_cli-0.19.0/tests/test_gateway_client.py +52 -0
  14. swarph_cli-0.19.0/tests/test_lane_command.py +101 -0
  15. swarph_cli-0.19.0/tests/test_schedule_command.py +163 -0
  16. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/LICENSE +0 -0
  17. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/setup.cfg +0 -0
  18. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/caller.py +0 -0
  19. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/__init__.py +0 -0
  20. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/harden.py +0 -0
  21. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/lineage.py +0 -0
  22. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/liveness.py +0 -0
  23. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/manifest.py +0 -0
  24. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/paths.py +0 -0
  25. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/capture/verify.py +0 -0
  26. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/cell.py +0 -0
  27. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/__init__.py +0 -0
  28. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/add.py +0 -0
  29. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/brain.py +0 -0
  30. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/brain_ask.py +0 -0
  31. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/cell.py +0 -0
  32. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/chat.py +0 -0
  33. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/compress.py +0 -0
  34. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/daemon.py +0 -0
  35. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/gateway.py +0 -0
  36. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/highlight.py +0 -0
  37. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/hook_output.py +0 -0
  38. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/hooks.py +0 -0
  39. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/import_session.py +0 -0
  40. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/init.py +0 -0
  41. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/install_hook.py +0 -0
  42. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  43. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  44. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/mesh.py +0 -0
  45. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/onboard.py +0 -0
  46. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  47. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/ratify.py +0 -0
  48. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/security.py +0 -0
  49. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/service.py +0 -0
  50. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/spawn.py +0 -0
  51. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/commands/watchdog.py +0 -0
  52. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/__init__.py +0 -0
  53. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/levers.py +0 -0
  54. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/marker.py +0 -0
  55. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/compress/verify.py +0 -0
  56. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/__init__.py +0 -0
  57. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  58. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  59. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/schema.sql +0 -0
  60. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/server.py +0 -0
  61. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/gateway/services_control.py +0 -0
  62. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/parsers/__init__.py +0 -0
  63. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/parsers/claude.py +0 -0
  64. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/service/__init__.py +0 -0
  65. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/service/app.py +0 -0
  66. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/service/providers.py +0 -0
  67. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  68. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  69. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  70. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  71. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  72. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  73. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  74. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_add_security_gate.py +0 -0
  75. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_add.py +0 -0
  76. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_hash.py +0 -0
  77. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_lib.py +0 -0
  78. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_mcp.py +0 -0
  79. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_skill.py +0 -0
  80. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_tool.py +0 -0
  81. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_artifact_uri.py +0 -0
  82. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_brain_ask_command.py +0 -0
  83. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_brain_command.py +0 -0
  84. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_caller_meta_guard.py +0 -0
  85. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_lineage.py +0 -0
  86. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_liveness.py +0 -0
  87. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_manifest.py +0 -0
  88. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_paths.py +0 -0
  89. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_capture_security.py +0 -0
  90. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_command_dispatch.py +0 -0
  91. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_harden.py +0 -0
  92. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_loader.py +0 -0
  93. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_cell_verify.py +0 -0
  94. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_chat_command.py +0 -0
  95. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_claude_parser.py +0 -0
  96. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_claude_tmux_template.py +0 -0
  97. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_caller_convention.py +0 -0
  98. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_command.py +0 -0
  99. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_levers.py +0 -0
  100. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_marker.py +0 -0
  101. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_compress_verify.py +0 -0
  102. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_daemon_command.py +0 -0
  103. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_feature_to_uri.py +0 -0
  104. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_gateway_command.py +0 -0
  105. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_highlight_command.py +0 -0
  106. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hook_output.py +0 -0
  107. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_add.py +0 -0
  108. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_bundle.py +0 -0
  109. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_init.py +0 -0
  110. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_lifecycle.py +0 -0
  111. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_hooks_merge.py +0 -0
  112. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_import_command.py +0 -0
  113. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_init_command.py +0 -0
  114. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_install_hook.py +0 -0
  115. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_main.py +0 -0
  116. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_mcp_server.py +0 -0
  117. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_memory_sync.py +0 -0
  118. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_mesh_command.py +0 -0
  119. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_mesh_sidecar.py +0 -0
  120. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_onboard_command.py +0 -0
  121. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_protocol_handler.py +0 -0
  122. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_ratify_command.py +0 -0
  123. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_repl_caller_convention.py +0 -0
  124. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_security.py +0 -0
  125. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_service_app.py +0 -0
  126. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_service_command.py +0 -0
  127. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_service_providers.py +0 -0
  128. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_session_path_role_gate.py +0 -0
  129. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_smoke_chat.py +0 -0
  130. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_smoke_one_shot.py +0 -0
  131. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_smoke_phase_5_5.py +0 -0
  132. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_command.py +0 -0
  133. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_live_pin.py +0 -0
  134. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  135. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_tmux_session.py +0 -0
  136. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_spawn_windows_relaunch.py +0 -0
  137. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_version_consistency.py +0 -0
  138. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog.py +0 -0
  139. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_dm_wake.py +0 -0
  140. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  141. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  142. {swarph_cli-0.18.0 → swarph_cli-0.19.0}/tests/test_watchdog_model_rung.py +0 -0
  143. {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.18.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.18.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" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.18.0"
19
+ __version__ = "0.19.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -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
  }