swarph-cli 0.18.0__tar.gz → 0.20.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 (147) hide show
  1. {swarph_cli-0.18.0/src/swarph_cli.egg-info → swarph_cli-0.20.0}/PKG-INFO +32 -1
  2. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/README.md +31 -0
  3. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/pyproject.toml +1 -1
  4. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/__init__.py +1 -1
  5. swarph_cli-0.20.0/src/swarph_cli/commands/_gateway_client.py +38 -0
  6. swarph_cli-0.20.0/src/swarph_cli/commands/channel.py +154 -0
  7. swarph_cli-0.20.0/src/swarph_cli/commands/install_multiplexer.py +59 -0
  8. swarph_cli-0.20.0/src/swarph_cli/commands/lane.py +175 -0
  9. swarph_cli-0.20.0/src/swarph_cli/commands/schedule.py +185 -0
  10. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/spawn.py +8 -0
  11. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/main.py +4 -0
  12. swarph_cli-0.20.0/src/swarph_cli/multiplexer.py +138 -0
  13. {swarph_cli-0.18.0 → swarph_cli-0.20.0/src/swarph_cli.egg-info}/PKG-INFO +32 -1
  14. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli.egg-info/SOURCES.txt +12 -0
  15. swarph_cli-0.20.0/tests/test_channel_command.py +141 -0
  16. swarph_cli-0.20.0/tests/test_gateway_client.py +52 -0
  17. swarph_cli-0.20.0/tests/test_install_multiplexer_command.py +77 -0
  18. swarph_cli-0.20.0/tests/test_lane_command.py +101 -0
  19. swarph_cli-0.20.0/tests/test_multiplexer.py +144 -0
  20. swarph_cli-0.20.0/tests/test_schedule_command.py +163 -0
  21. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/LICENSE +0 -0
  22. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/setup.cfg +0 -0
  23. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/caller.py +0 -0
  24. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/__init__.py +0 -0
  25. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/harden.py +0 -0
  26. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/lineage.py +0 -0
  27. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/liveness.py +0 -0
  28. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/manifest.py +0 -0
  29. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/paths.py +0 -0
  30. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/capture/verify.py +0 -0
  31. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/cell.py +0 -0
  32. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/__init__.py +0 -0
  33. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/add.py +0 -0
  34. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/brain.py +0 -0
  35. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/brain_ask.py +0 -0
  36. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/cell.py +0 -0
  37. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/chat.py +0 -0
  38. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/compress.py +0 -0
  39. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/daemon.py +0 -0
  40. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/gateway.py +0 -0
  41. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/highlight.py +0 -0
  42. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/hook_output.py +0 -0
  43. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/hooks.py +0 -0
  44. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/import_session.py +0 -0
  45. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/init.py +0 -0
  46. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/install_hook.py +0 -0
  47. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  48. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  49. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/mesh.py +0 -0
  50. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/onboard.py +0 -0
  51. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  52. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/ratify.py +0 -0
  53. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/security.py +0 -0
  54. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/service.py +0 -0
  55. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/commands/watchdog.py +0 -0
  56. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/compress/__init__.py +0 -0
  57. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/compress/levers.py +0 -0
  58. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/compress/marker.py +0 -0
  59. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/compress/verify.py +0 -0
  60. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/gateway/__init__.py +0 -0
  61. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  62. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  63. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/gateway/schema.sql +0 -0
  64. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/gateway/server.py +0 -0
  65. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/gateway/services_control.py +0 -0
  66. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/parsers/__init__.py +0 -0
  67. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/parsers/claude.py +0 -0
  68. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/service/__init__.py +0 -0
  69. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/service/app.py +0 -0
  70. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/service/providers.py +0 -0
  71. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  72. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  73. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  74. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  75. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  76. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  77. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  78. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_add_security_gate.py +0 -0
  79. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_add.py +0 -0
  80. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_hash.py +0 -0
  81. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_lib.py +0 -0
  82. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_mcp.py +0 -0
  83. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_skill.py +0 -0
  84. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_tool.py +0 -0
  85. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_artifact_uri.py +0 -0
  86. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_brain_ask_command.py +0 -0
  87. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_brain_command.py +0 -0
  88. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_caller_meta_guard.py +0 -0
  89. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_capture_lineage.py +0 -0
  90. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_capture_liveness.py +0 -0
  91. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_capture_manifest.py +0 -0
  92. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_capture_paths.py +0 -0
  93. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_capture_security.py +0 -0
  94. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_cell_command_dispatch.py +0 -0
  95. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_cell_harden.py +0 -0
  96. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_cell_loader.py +0 -0
  97. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_cell_verify.py +0 -0
  98. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_chat_command.py +0 -0
  99. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_claude_parser.py +0 -0
  100. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_claude_tmux_template.py +0 -0
  101. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_compress_caller_convention.py +0 -0
  102. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_compress_command.py +0 -0
  103. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_compress_levers.py +0 -0
  104. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_compress_marker.py +0 -0
  105. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_compress_verify.py +0 -0
  106. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_daemon_command.py +0 -0
  107. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_feature_to_uri.py +0 -0
  108. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_gateway_command.py +0 -0
  109. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_highlight_command.py +0 -0
  110. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_hook_output.py +0 -0
  111. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_hooks_add.py +0 -0
  112. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_hooks_bundle.py +0 -0
  113. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_hooks_init.py +0 -0
  114. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_hooks_lifecycle.py +0 -0
  115. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_hooks_merge.py +0 -0
  116. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_import_command.py +0 -0
  117. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_init_command.py +0 -0
  118. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_install_hook.py +0 -0
  119. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_main.py +0 -0
  120. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_mcp_server.py +0 -0
  121. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_memory_sync.py +0 -0
  122. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_mesh_command.py +0 -0
  123. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_mesh_sidecar.py +0 -0
  124. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_onboard_command.py +0 -0
  125. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_protocol_handler.py +0 -0
  126. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_ratify_command.py +0 -0
  127. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_repl_caller_convention.py +0 -0
  128. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_security.py +0 -0
  129. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_service_app.py +0 -0
  130. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_service_command.py +0 -0
  131. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_service_providers.py +0 -0
  132. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_session_path_role_gate.py +0 -0
  133. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_smoke_chat.py +0 -0
  134. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_smoke_one_shot.py +0 -0
  135. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_smoke_phase_5_5.py +0 -0
  136. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_spawn_command.py +0 -0
  137. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_spawn_live_pin.py +0 -0
  138. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  139. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_spawn_tmux_session.py +0 -0
  140. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_spawn_windows_relaunch.py +0 -0
  141. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_version_consistency.py +0 -0
  142. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_watchdog.py +0 -0
  143. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_watchdog_dm_wake.py +0 -0
  144. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  145. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  146. {swarph_cli-0.18.0 → swarph_cli-0.20.0}/tests/test_watchdog_model_rung.py +0 -0
  147. {swarph_cli-0.18.0 → swarph_cli-0.20.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.20.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
@@ -51,6 +51,8 @@ pip install swarph-cli
51
51
  swarph --version
52
52
  ```
53
53
 
54
+ > **System requirement — a terminal multiplexer.** The session verbs (`spawn`, `cell`, `watchdog`) drive a `tmux`-compatible multiplexer. On **Linux/macOS** install `tmux` (`apt install tmux` / `brew install tmux`). On **Windows** there is no native tmux — install [**psmux**](https://github.com/psmux/psmux) (a Rust tmux-for-Windows, MIT) with `swarph install-multiplexer` (fetches the pinned, checksum-verified binary into `~/.swarph/bin`) or `winget install marlocarlo.psmux`.
55
+
54
56
  **What it is:** a multi-provider LLM command line. Run one-shot prompts or an interactive REPL against any supported provider; spawn long-lived agent *cells* that persist across restarts and coordinate over a mesh; install hooks, MCP servers, and skills by content-addressed URI. An open, inspectable substrate — not a closed orchestration platform.
55
57
 
56
58
  **Who it's for:** builders running more than one LLM who want them to *cooperate* instead of sitting in separate tabs — multi-agent systems across vendors, agnostic by design. A thin client over the [`swarph-mesh`](https://github.com/BrainSurfing-tech/swarph-mesh) substrate library.
@@ -72,10 +74,14 @@ swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited an
72
74
  swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
73
75
  swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
74
76
  swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
77
+ swarph channel <sub> mesh channels — create/join/leave/list/members
78
+ swarph schedule <sub> scheduled events — create/list/get/enable/disable/delete/fire-now
79
+ swarph lane <sub> $0-lane orchestration — list/create/scale/delete/enqueue
75
80
  swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
76
81
  swarph spawn <role> launch a long-lived agent session as a named mesh cell
77
82
  swarph daemon foreground inbox-drain loop (the mesh doorbell)
78
83
  swarph watchdog detect + recover stranded agent sessions (cron- or systemd-driven)
84
+ swarph install-multiplexer fetch the Windows tmux multiplexer (psmux) — pinned + checksum-verified
79
85
  swarph add <uri> install a hook / MCP server / skill by content-addressed URI
80
86
  swarph hooks install Claude Code hooks as reusable artifacts
81
87
  swarph onboard / ratify bring a new peer into the mesh (mechanics + witness ratification)
@@ -138,6 +144,31 @@ $ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
138
144
 
139
145
  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
146
 
147
+ ### `swarph channel` / `swarph schedule` / `swarph lane`
148
+
149
+ 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.
150
+
151
+ ```bash
152
+ # channels — converge work into pub/sub rooms
153
+ $ swarph channel create research --kind topic --description "market-structure notes"
154
+ $ swarph channel join research --wake-policy mentions_only
155
+ $ swarph channel list
156
+
157
+ # scheduled events — operator-gated recurring/ conditional fires (a 403 is surfaced verbatim)
158
+ $ swarph schedule create nightly-digest --trigger time --cron "0 7 * * *" \
159
+ --target lab-ovh --task "compile the overnight digest" --context "[[some-anchor]]"
160
+ $ swarph schedule enable nightly-digest
161
+ $ swarph schedule fire-now nightly-digest
162
+
163
+ # lanes — drive the gateway's $0 worker pools
164
+ $ swarph lane list
165
+ $ swarph lane create judges --provider claude --model sonnet --n 3
166
+ $ swarph lane enqueue judges --prompt "score this design ..."
167
+ $ swarph lane scale judges --n 0
168
+ ```
169
+
170
+ 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.
171
+
141
172
  ### `swarph highlight`
142
173
 
143
174
  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.
@@ -7,6 +7,8 @@ pip install swarph-cli
7
7
  swarph --version
8
8
  ```
9
9
 
10
+ > **System requirement — a terminal multiplexer.** The session verbs (`spawn`, `cell`, `watchdog`) drive a `tmux`-compatible multiplexer. On **Linux/macOS** install `tmux` (`apt install tmux` / `brew install tmux`). On **Windows** there is no native tmux — install [**psmux**](https://github.com/psmux/psmux) (a Rust tmux-for-Windows, MIT) with `swarph install-multiplexer` (fetches the pinned, checksum-verified binary into `~/.swarph/bin`) or `winget install marlocarlo.psmux`.
11
+
10
12
  **What it is:** a multi-provider LLM command line. Run one-shot prompts or an interactive REPL against any supported provider; spawn long-lived agent *cells* that persist across restarts and coordinate over a mesh; install hooks, MCP servers, and skills by content-addressed URI. An open, inspectable substrate — not a closed orchestration platform.
11
13
 
12
14
  **Who it's for:** builders running more than one LLM who want them to *cooperate* instead of sitting in separate tabs — multi-agent systems across vendors, agnostic by design. A thin client over the [`swarph-mesh`](https://github.com/BrainSurfing-tech/swarph-mesh) substrate library.
@@ -28,10 +30,14 @@ swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited an
28
30
  swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
29
31
  swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
30
32
  swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
33
+ swarph channel <sub> mesh channels — create/join/leave/list/members
34
+ swarph schedule <sub> scheduled events — create/list/get/enable/disable/delete/fire-now
35
+ swarph lane <sub> $0-lane orchestration — list/create/scale/delete/enqueue
31
36
  swarph highlight "<x>" log a highlight to the shared git-backed swarph timeline
32
37
  swarph spawn <role> launch a long-lived agent session as a named mesh cell
33
38
  swarph daemon foreground inbox-drain loop (the mesh doorbell)
34
39
  swarph watchdog detect + recover stranded agent sessions (cron- or systemd-driven)
40
+ swarph install-multiplexer fetch the Windows tmux multiplexer (psmux) — pinned + checksum-verified
35
41
  swarph add <uri> install a hook / MCP server / skill by content-addressed URI
36
42
  swarph hooks install Claude Code hooks as reusable artifacts
37
43
  swarph onboard / ratify bring a new peer into the mesh (mechanics + witness ratification)
@@ -94,6 +100,31 @@ $ curl -s localhost:8799/delegate -H "Authorization: Bearer $TOK" \
94
100
 
95
101
  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
102
 
103
+ ### `swarph channel` / `swarph schedule` / `swarph lane`
104
+
105
+ 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.
106
+
107
+ ```bash
108
+ # channels — converge work into pub/sub rooms
109
+ $ swarph channel create research --kind topic --description "market-structure notes"
110
+ $ swarph channel join research --wake-policy mentions_only
111
+ $ swarph channel list
112
+
113
+ # scheduled events — operator-gated recurring/ conditional fires (a 403 is surfaced verbatim)
114
+ $ swarph schedule create nightly-digest --trigger time --cron "0 7 * * *" \
115
+ --target lab-ovh --task "compile the overnight digest" --context "[[some-anchor]]"
116
+ $ swarph schedule enable nightly-digest
117
+ $ swarph schedule fire-now nightly-digest
118
+
119
+ # lanes — drive the gateway's $0 worker pools
120
+ $ swarph lane list
121
+ $ swarph lane create judges --provider claude --model sonnet --n 3
122
+ $ swarph lane enqueue judges --prompt "score this design ..."
123
+ $ swarph lane scale judges --n 0
124
+ ```
125
+
126
+ 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.
127
+
97
128
  ### `swarph highlight`
98
129
 
99
130
  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.20.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.20.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,59 @@
1
+ """``swarph install-multiplexer`` — fetch the checksum-verified psmux binary.
2
+
3
+ swarph's session machinery needs a ``tmux``-compatible multiplexer on PATH. On
4
+ Linux/macOS that's native ``tmux`` (install via apt/brew). On Windows there's no
5
+ pip/npm package, so this verb fetches the pinned, SHA-256-verified **psmux**
6
+ release zip and drops its ``tmux.exe`` into ``--dir`` (default
7
+ ``~/.swarph/bin``). The download is verified before anything is written.
8
+
9
+ Usage:
10
+ swarph install-multiplexer [--dir DIR] [--force] [--version 3.3.6]
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import os
17
+ import sys
18
+
19
+ from swarph_cli.multiplexer import (
20
+ PSMUX_VERSION,
21
+ find_multiplexer,
22
+ install_psmux,
23
+ )
24
+
25
+
26
+ def run_install_multiplexer(argv: list) -> int:
27
+ p = argparse.ArgumentParser(
28
+ prog="swarph install-multiplexer",
29
+ description="Fetch the checksum-verified psmux (tmux-for-Windows) binary.")
30
+ p.add_argument("--dir", default=os.path.expanduser("~/.swarph/bin"),
31
+ help="install directory (default ~/.swarph/bin)")
32
+ p.add_argument("--force", action="store_true",
33
+ help="reinstall even if a multiplexer is already on PATH")
34
+ p.add_argument("--version", default=PSMUX_VERSION,
35
+ help=f"psmux version to fetch (default {PSMUX_VERSION})")
36
+ args = p.parse_args(argv)
37
+
38
+ if sys.platform != "win32":
39
+ print("swarph install-multiplexer fetches the Windows psmux binary. On "
40
+ "Linux/macOS, tmux is the native multiplexer — install it via your "
41
+ "package manager (`apt install tmux` / `brew install tmux`).",
42
+ file=sys.stderr)
43
+ return 0
44
+
45
+ if not args.force:
46
+ present = find_multiplexer()
47
+ if present:
48
+ print(f"multiplexer already present at {present}")
49
+ return 0
50
+
51
+ try:
52
+ path = install_psmux(args.dir, version=args.version)
53
+ except (RuntimeError, ValueError, OSError) as err:
54
+ print(f"swarph install-multiplexer: {err}", file=sys.stderr)
55
+ return 1
56
+
57
+ print(f"installed psmux {args.version} -> {path}; "
58
+ f"add {args.dir} to your PATH")
59
+ 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