swarph-cli 0.23.0__tar.gz → 0.24.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 (155) hide show
  1. {swarph_cli-0.23.0/src/swarph_cli.egg-info → swarph_cli-0.24.0}/PKG-INFO +15 -1
  2. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/README.md +14 -0
  3. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/pyproject.toml +1 -1
  4. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/__init__.py +1 -1
  5. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/chain_token.py +8 -4
  6. swarph_cli-0.24.0/src/swarph_cli/commands/codegraph.py +194 -0
  7. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/mcp_server.py +60 -5
  8. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/main.py +1 -0
  9. {swarph_cli-0.23.0 → swarph_cli-0.24.0/src/swarph_cli.egg-info}/PKG-INFO +15 -1
  10. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/SOURCES.txt +3 -0
  11. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_chain_token.py +7 -0
  12. swarph_cli-0.24.0/tests/test_codegraph_command.py +107 -0
  13. swarph_cli-0.24.0/tests/test_codegraph_mcp.py +98 -0
  14. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/LICENSE +0 -0
  15. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/setup.cfg +0 -0
  16. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/caller.py +0 -0
  17. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/__init__.py +0 -0
  18. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/harden.py +0 -0
  19. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/lineage.py +0 -0
  20. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/liveness.py +0 -0
  21. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/manifest.py +0 -0
  22. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/paths.py +0 -0
  23. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/verify.py +0 -0
  24. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/cell.py +0 -0
  25. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/__init__.py +0 -0
  26. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
  27. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/add.py +0 -0
  28. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/brain.py +0 -0
  29. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/brain_ask.py +0 -0
  30. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/cell.py +0 -0
  31. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/channel.py +0 -0
  32. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/chat.py +0 -0
  33. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/compress.py +0 -0
  34. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/daemon.py +0 -0
  35. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/event.py +0 -0
  36. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/gateway.py +0 -0
  37. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/highlight.py +0 -0
  38. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/hook_output.py +0 -0
  39. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/hooks.py +0 -0
  40. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/import_session.py +0 -0
  41. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/init.py +0 -0
  42. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/install_hook.py +0 -0
  43. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  44. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/lane.py +0 -0
  45. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  46. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/mesh.py +0 -0
  47. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/onboard.py +0 -0
  48. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  49. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/ratify.py +0 -0
  50. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/schedule.py +0 -0
  51. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/security.py +0 -0
  52. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/service.py +0 -0
  53. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/spawn.py +0 -0
  54. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/watchdog.py +0 -0
  55. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/__init__.py +0 -0
  56. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/levers.py +0 -0
  57. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/marker.py +0 -0
  58. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/verify.py +0 -0
  59. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/__init__.py +0 -0
  60. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  61. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  62. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/schema.sql +0 -0
  63. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/server.py +0 -0
  64. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/services_control.py +0 -0
  65. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/multiplexer.py +0 -0
  66. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/parsers/__init__.py +0 -0
  67. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/parsers/claude.py +0 -0
  68. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/service/__init__.py +0 -0
  69. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/service/app.py +0 -0
  70. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/service/providers.py +0 -0
  71. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  72. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  73. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  74. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  75. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  76. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  77. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  78. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_add_security_gate.py +0 -0
  79. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_add.py +0 -0
  80. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_hash.py +0 -0
  81. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_lib.py +0 -0
  82. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_mcp.py +0 -0
  83. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_skill.py +0 -0
  84. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_tool.py +0 -0
  85. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_uri.py +0 -0
  86. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_brain_ask_command.py +0 -0
  87. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_brain_command.py +0 -0
  88. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_caller_meta_guard.py +0 -0
  89. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_lineage.py +0 -0
  90. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_liveness.py +0 -0
  91. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_manifest.py +0 -0
  92. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_paths.py +0 -0
  93. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_security.py +0 -0
  94. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_command_dispatch.py +0 -0
  95. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_harden.py +0 -0
  96. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_loader.py +0 -0
  97. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_verify.py +0 -0
  98. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_channel_command.py +0 -0
  99. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_chat_command.py +0 -0
  100. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_claude_parser.py +0 -0
  101. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_claude_tmux_template.py +0 -0
  102. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_caller_convention.py +0 -0
  103. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_command.py +0 -0
  104. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_levers.py +0 -0
  105. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_marker.py +0 -0
  106. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_verify.py +0 -0
  107. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_daemon_command.py +0 -0
  108. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_event_emit.py +0 -0
  109. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_feature_to_uri.py +0 -0
  110. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_gateway_client.py +0 -0
  111. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_gateway_command.py +0 -0
  112. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_highlight_command.py +0 -0
  113. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hook_output.py +0 -0
  114. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_add.py +0 -0
  115. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_bundle.py +0 -0
  116. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_init.py +0 -0
  117. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_lifecycle.py +0 -0
  118. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_merge.py +0 -0
  119. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_import_command.py +0 -0
  120. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_init_command.py +0 -0
  121. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_install_hook.py +0 -0
  122. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_install_multiplexer_command.py +0 -0
  123. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_lane_command.py +0 -0
  124. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_main.py +0 -0
  125. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_mcp_server.py +0 -0
  126. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_memory_sync.py +0 -0
  127. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_mesh_command.py +0 -0
  128. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_mesh_sidecar.py +0 -0
  129. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_meta_edge_identity.py +0 -0
  130. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_multiplexer.py +0 -0
  131. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_onboard_command.py +0 -0
  132. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_protocol_handler.py +0 -0
  133. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_ratify_command.py +0 -0
  134. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_repl_caller_convention.py +0 -0
  135. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_schedule_command.py +0 -0
  136. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_security.py +0 -0
  137. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_service_app.py +0 -0
  138. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_service_command.py +0 -0
  139. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_service_providers.py +0 -0
  140. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_session_path_role_gate.py +0 -0
  141. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_smoke_chat.py +0 -0
  142. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_smoke_one_shot.py +0 -0
  143. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_smoke_phase_5_5.py +0 -0
  144. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_command.py +0 -0
  145. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_live_pin.py +0 -0
  146. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  147. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_tmux_session.py +0 -0
  148. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_windows_relaunch.py +0 -0
  149. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_version_consistency.py +0 -0
  150. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog.py +0 -0
  151. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_dm_wake.py +0 -0
  152. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  153. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  154. {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_model_rung.py +0 -0
  155. {swarph_cli-0.23.0 → swarph_cli-0.24.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.23.0
3
+ Version: 0.24.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,7 @@ This is one of three repos in the v0.3.x architecture:
72
72
  swarph "prompt" one-shot against any provider (claude/openai/gemini/deepseek/grok)
73
73
  swarph chat interactive REPL — multi-turn, slash commands, live provider switch
74
74
  swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited answer or raw chunks
75
+ swarph codegraph "<q>" structural code search — find where a symbol is defined / what calls it
75
76
  swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
76
77
  swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
77
78
  swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
@@ -117,6 +118,19 @@ $ swarph brain serve --gbrain-bin /opt/gbrain # explicit binary (else GBR
117
118
 
118
119
  Defaults are loopback bind (expose a tailnet IP explicitly) and a **1-year token TTL** — a short TTL silently 401s long-lived mesh cells, a lesson learned the hard way. If `gbrain` isn't on `PATH` the verb prints an install hint and exits `2`; `brain-ask` still queries any already-running brain.
119
120
 
121
+ ### `swarph codegraph` (v0.24.0)
122
+
123
+ Structural code search over a local **CodeGraph** index — the *what-where* hemisphere of the swarph brain. Ask in natural language where a function/class/symbol is **defined** or what **calls** it; get ranked hits with file:line, signature, and caller count.
124
+
125
+ ```bash
126
+ $ swarph codegraph "which function escapes special characters for HTML"
127
+ $ swarph codegraph "cron expression validator" --json --limit 5
128
+ ```
129
+
130
+ It queries a locally-built index at `~/.swarph/codegraph/index.db` (override with `--index` or `SWARPH_CODEGRAPH_INDEX`); the index is built out-of-band from your repos (tree-sitter → SQLite FTS5, BM25-ranked). If no index is present the verb simply returns no matches — it never touches the network and never reads code it wasn't pointed at. Results are scoped by an owner-allowlist gate (`--caller-cell` / `SWARPH_CELL`, default: your cell): public repos are always visible; private repos are visible to the owning cell.
131
+
132
+ The same capability is exposed as an **MCP tool** — running `swarph mcp-server` publishes `swarph_codegraph_query` alongside `swarph_search`/`swarph_add`/`swarph_describe`, so any MCP host's agent auto-discovers it and can reach for code-structure lookups while reading, writing, or debugging code. (Why a tool and not an always-on retrieval lane? Because *intent* to consult code structure lives with the calling agent — which already knows it's working on code — not in a similarity score.)
133
+
120
134
  ### `swarph gateway`
121
135
 
122
136
  The **mesh-gateway** is the coordination/DM server every other verb talks to — the peer registry, the DM inbox/outbox, feature aggregation with allowlist + caps, and lane/service control. It used to be a separate deployment; it's now bundled, so any host can stand one up with a single command. It pairs with the client verbs (`mesh`, `spawn`, `daemon`, `watchdog`) and with `brain-ask`/gbrain to form the mesh's coordination plane.
@@ -27,6 +27,7 @@ This is one of three repos in the v0.3.x architecture:
27
27
  swarph "prompt" one-shot against any provider (claude/openai/gemini/deepseek/grok)
28
28
  swarph chat interactive REPL — multi-turn, slash commands, live provider switch
29
29
  swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited answer or raw chunks
30
+ swarph codegraph "<q>" structural code search — find where a symbol is defined / what calls it
30
31
  swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
31
32
  swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
32
33
  swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
@@ -72,6 +73,19 @@ $ swarph brain serve --gbrain-bin /opt/gbrain # explicit binary (else GBR
72
73
 
73
74
  Defaults are loopback bind (expose a tailnet IP explicitly) and a **1-year token TTL** — a short TTL silently 401s long-lived mesh cells, a lesson learned the hard way. If `gbrain` isn't on `PATH` the verb prints an install hint and exits `2`; `brain-ask` still queries any already-running brain.
74
75
 
76
+ ### `swarph codegraph` (v0.24.0)
77
+
78
+ Structural code search over a local **CodeGraph** index — the *what-where* hemisphere of the swarph brain. Ask in natural language where a function/class/symbol is **defined** or what **calls** it; get ranked hits with file:line, signature, and caller count.
79
+
80
+ ```bash
81
+ $ swarph codegraph "which function escapes special characters for HTML"
82
+ $ swarph codegraph "cron expression validator" --json --limit 5
83
+ ```
84
+
85
+ It queries a locally-built index at `~/.swarph/codegraph/index.db` (override with `--index` or `SWARPH_CODEGRAPH_INDEX`); the index is built out-of-band from your repos (tree-sitter → SQLite FTS5, BM25-ranked). If no index is present the verb simply returns no matches — it never touches the network and never reads code it wasn't pointed at. Results are scoped by an owner-allowlist gate (`--caller-cell` / `SWARPH_CELL`, default: your cell): public repos are always visible; private repos are visible to the owning cell.
86
+
87
+ The same capability is exposed as an **MCP tool** — running `swarph mcp-server` publishes `swarph_codegraph_query` alongside `swarph_search`/`swarph_add`/`swarph_describe`, so any MCP host's agent auto-discovers it and can reach for code-structure lookups while reading, writing, or debugging code. (Why a tool and not an always-on retrieval lane? Because *intent* to consult code structure lives with the calling agent — which already knows it's working on code — not in a similarity score.)
88
+
75
89
  ### `swarph gateway`
76
90
 
77
91
  The **mesh-gateway** is the coordination/DM server every other verb talks to — the peer registry, the DM inbox/outbox, feature aggregation with allowlist + caps, and lane/service control. It used to be a separate deployment; it's now bundled, so any host can stand one up with a single command. It pairs with the client verbs (`mesh`, `spawn`, `daemon`, `watchdog`) and with `brain-ask`/gbrain to form the mesh's coordination plane.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.23.0"
7
+ version = "0.24.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.23.0"
19
+ __version__ = "0.24.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -21,6 +21,10 @@ def sign_chain_token(secret: str, chain_id: str, depth: int, jti: str) -> str:
21
21
 
22
22
 
23
23
  def verify_chain_token(secret: str, token: str) -> Optional[dict]:
24
+ # token is opaque untrusted input (network / mesh DM); a non-str value has no
25
+ # .split and would AttributeError past the except below. Guard explicitly.
26
+ if not isinstance(token, str):
27
+ return None
24
28
  try:
25
29
  body_s, sig_s = token.split(".", 1)
26
30
  payload = _unb64(body_s)
@@ -31,8 +35,8 @@ def verify_chain_token(secret: str, token: str) -> Optional[dict]:
31
35
  if not isinstance(d, dict) or {"chain_id", "depth", "jti"} - d.keys():
32
36
  return None
33
37
  return {"chain_id": d["chain_id"], "depth": int(d["depth"]), "jti": d["jti"]}
34
- except (ValueError, TypeError, KeyError, json.JSONDecodeError):
35
- # the known malformed-input paths (split / b64 / json / int / dict). Fail
36
- # safe to None. NOT a bare `Exception`: an unexpected error should surface
37
- # as a bug, not silently masquerade as "invalid token".
38
+ except (ValueError, TypeError, KeyError, AttributeError, json.JSONDecodeError):
39
+ # the known malformed-input paths (split / b64 / json / int / dict /
40
+ # non-str secret). Fail safe to None. NOT a bare `Exception`: an
41
+ # unexpected error should surface as a bug, not masquerade as "invalid".
38
42
  return None
@@ -0,0 +1,194 @@
1
+ """``swarph codegraph`` — on-demand STRUCTURAL code search over a LOCAL
2
+ derived index.
3
+
4
+ Self-contained port: this module does NOT depend on the local-only
5
+ ``codegraph_index`` package. The query/text-search/sensitivity logic below
6
+ is ported verbatim (imports adapted only) so swarph-cli — a PUBLIC PyPI
7
+ package — stays free of any dependency on that local tool.
8
+
9
+ Retrieval is a TOOL an agent invokes when it needs to find where a symbol
10
+ is defined or what calls it, not a per-prompt fusion lane — the intent to
11
+ consult code structure comes from the caller.
12
+
13
+ Fail-safe (A2): the query path is READ-ONLY, never raises, never hangs.
14
+ A missing index file, a corrupt db, a malformed FTS5 MATCH string, or a
15
+ locked db all resolve to ``[]`` — never an exception.
16
+
17
+ Sensitivity gate (A8): ``_visible_repos`` decides which repos a caller may
18
+ see BEFORE any row is read — public repos are always visible; private
19
+ repos require the caller's cell to be present in ``allowlist[repo]``. The
20
+ default allowlist (``_local_allowlist``) is operate-what-you-own: the
21
+ local caller sees every repo present in its own index.
22
+
23
+ Usage:
24
+ swarph codegraph "which function renders the thing"
25
+ swarph codegraph "cron expression validator" --json --limit 5
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import os
33
+ import re
34
+ import sqlite3
35
+
36
+ DEFAULT_INDEX = os.path.expanduser("~/.swarph/codegraph/index.db")
37
+ # NOTE: named DEFAULT_CALLER_CELL, not "*_CALLER" — this is an A8 caller-CELL
38
+ # identity (sensitivity-gate scoping), not a SwarphCall `role.subrole.specific`
39
+ # caller tag, and must NOT trip the repo-wide caller-convention meta-guard
40
+ # (tests/test_caller_meta_guard.py) which validates every `*_CALLER` constant
41
+ # against that unrelated dotted-slug convention.
42
+ DEFAULT_CALLER_CELL = "lab-ovh"
43
+
44
+ # --- textsearch (ported verbatim from codegraph_index/textsearch.py) ------
45
+
46
+ _STOP = {"the", "a", "an", "is", "are", "was", "how", "does", "do", "of", "to", "in", "on", "for", "and", "or",
47
+ "what", "which", "where", "when", "that", "this", "it", "its", "with", "by", "from", "get", "set"}
48
+
49
+
50
+ def _tokens(nl: str) -> list:
51
+ """Sanitized content-tokens (lowercased, punctuation-stripped, stopwords
52
+ dropped, order-preserving de-dupe)."""
53
+ toks = re.findall(r"[A-Za-z0-9]+", (nl or "").lower())
54
+ toks = [t for t in toks if len(t) > 1 and t not in _STOP]
55
+ return list(dict.fromkeys(toks)) # dedupe, preserve first-seen order
56
+
57
+
58
+ def _sanitize_query(nl: str) -> str:
59
+ """Turn free-text NL into an FTS5 MATCH string: OR-joined terms,
60
+ punctuation stripped, stopwords dropped, order-preserving de-dupe.
61
+ Empty input -> "" (caller must treat that as "no query", never issue a
62
+ MATCH on an empty string)."""
63
+ return " OR ".join(_tokens(nl))
64
+
65
+
66
+ # --- connection (ported from codegraph_index/schema.py, read-only) --------
67
+
68
+ def _connect(index_path: str, timeout_ms: int = 100) -> sqlite3.Connection:
69
+ con = sqlite3.connect(f"file:{index_path}?mode=ro", uri=True, timeout=timeout_ms / 1000)
70
+ con.execute(f"PRAGMA busy_timeout={timeout_ms}") # A2: bound the wait, never block
71
+ return con
72
+
73
+
74
+ # --- sensitivity (ported verbatim from codegraph_index/sensitivity.py) ----
75
+
76
+ def _visible_repos(con, caller_cell: str, allowlist: dict) -> set:
77
+ out = set()
78
+ for name, vis in con.execute("SELECT name, visibility FROM repos"):
79
+ if vis == "public" or caller_cell in allowlist.get(name, []):
80
+ out.add(name)
81
+ return out
82
+
83
+
84
+ # --- query (ported verbatim from codegraph_index/query.py) ----------------
85
+
86
+ def structural_query(term, *, index_path, caller_cell, limit=8, allowlist=None) -> list:
87
+ """Ranked structural search. A8-gated, A2 fail-safe: any sqlite3.Error
88
+ (missing index, corrupt db, malformed MATCH, locked db) -> []."""
89
+ if allowlist is None:
90
+ allowlist = _local_allowlist(index_path, caller_cell)
91
+ try:
92
+ con = _connect(index_path)
93
+ except sqlite3.Error:
94
+ return []
95
+ try:
96
+ allowed = _visible_repos(con, caller_cell, allowlist) # A8: gate BEFORE any row leaves
97
+ if not allowed:
98
+ return []
99
+ match = _sanitize_query(term)
100
+ if not match:
101
+ return []
102
+ qmarks = ",".join("?" * len(allowed))
103
+ rows = con.execute(
104
+ f"SELECT s.id,s.repo,s.name,s.kind,s.file_path,s.start_line,s.qualified_name,s.docstring,s.signature, "
105
+ f"bm25(symbols_fts, 10.0, 4.0, 1.0, 2.0) AS score "
106
+ f"FROM symbols_fts f JOIN symbols s ON s.id=f.rowid "
107
+ f"WHERE symbols_fts MATCH ? AND s.repo IN ({qmarks}) "
108
+ # 'import' nodes are references, never the *definition* a "which
109
+ # symbol implements X" query wants — same principle as the
110
+ # already-excluded 'file' kind.
111
+ f"AND s.kind NOT IN ('file','import') AND s.name NOT LIKE 'test\\_%' ESCAPE '\\' "
112
+ # test-file filter ANCHORED to a path segment/basename-start — an unanchored
113
+ # '%test_%' would blackhole real files like workers/backtest_engine.py (contains
114
+ # the substring 'test_'). Basename-after-a-slash starting test_, OR root-level test_*.
115
+ f"AND s.file_path NOT LIKE '%/tests/%' "
116
+ f"AND s.file_path NOT LIKE '%/test\\_%' ESCAPE '\\' "
117
+ f"AND s.file_path NOT LIKE 'test\\_%' ESCAPE '\\' "
118
+ f"ORDER BY bm25(symbols_fts, 10.0, 4.0, 1.0, 2.0) LIMIT ?",
119
+ [match, *sorted(allowed), limit]).fetchall()
120
+ out = []
121
+ for sid, repo, name, kind, fp, line, qualified_name, docstring, signature, score in rows:
122
+ # Only real call edges count as "callers" — the derived index carries CodeGraph's
123
+ # verbatim edge kinds (contains/imports/decorates/references/calls/...), and counting
124
+ # all of them would pollute the blast-radius number. 'calls' (plural) is CodeGraph's
125
+ # actual call-edge kind.
126
+ callers = con.execute(
127
+ "SELECT COUNT(*) FROM edges WHERE dst_symbol=? AND edge_type='calls'", (sid,)
128
+ ).fetchone()[0]
129
+ out.append({"repo": repo, "name": name, "kind": kind,
130
+ "file_path": fp, "start_line": line, "callers": callers, "score": score,
131
+ "qualified_name": qualified_name or "", "docstring": docstring or "",
132
+ "signature": signature or ""})
133
+ return out
134
+ # OperationalError (lock/busy, or a malformed FTS5 MATCH query string) OR DatabaseError
135
+ # (corrupt/non-sqlite file) — both are sqlite3.Error siblings — fail safe (A2), never raise/hang.
136
+ except sqlite3.Error:
137
+ return []
138
+ finally:
139
+ try:
140
+ con.close()
141
+ except Exception:
142
+ pass
143
+
144
+
145
+ def _local_allowlist(index_path: str, caller_cell: str) -> dict:
146
+ """Operate-what-you-own: the operator of the LOCAL index sees every repo
147
+ in it. A8's cross-cell scoping still applies to the caller_cell identity;
148
+ here the caller is the local operator, so grant it each repo present.
149
+ Fail-safe: unreadable index -> empty allowlist (structural_query then
150
+ returns [] via its own A8 gate)."""
151
+ try:
152
+ # A2: bound the connect + wait so a locked db can never hang the
153
+ # default query path (mirrors _connect's busy_timeout guarantee).
154
+ con = sqlite3.connect(f"file:{index_path}?mode=ro", uri=True, timeout=0.1)
155
+ con.execute("PRAGMA busy_timeout=100")
156
+ repos = [r[0] for r in con.execute("SELECT name FROM repos")]
157
+ con.close()
158
+ except sqlite3.Error:
159
+ return {}
160
+ return {r: [caller_cell] for r in repos}
161
+
162
+
163
+ def format_human(rows, term) -> str:
164
+ if not rows:
165
+ return f"No structural matches for: {term!r}"
166
+ lines = [f"Structural matches for {term!r} ({len(rows)}):"]
167
+ for i, r in enumerate(rows, 1):
168
+ loc = f"{r['file_path']}:{r['start_line']}"
169
+ n = r["callers"]
170
+ callers = f" ({n} caller{'s' if n != 1 else ''})" if n else ""
171
+ lines.append(f"{i}. {r['name']} [{r['kind']}] {r['repo']} {loc}{callers}")
172
+ if r["signature"]:
173
+ lines.append(f" {r['signature']}")
174
+ if r["docstring"]:
175
+ first = r["docstring"].strip().splitlines()[0][:100] if r["docstring"].strip() else ""
176
+ if first:
177
+ lines.append(f" {first}")
178
+ return "\n".join(lines)
179
+
180
+
181
+ def run_codegraph(argv) -> int:
182
+ ap = argparse.ArgumentParser(
183
+ prog="swarph codegraph",
184
+ description="On-demand structural code search: find where a symbol is defined or what calls it, "
185
+ "across the indexed repos. Natural-language query.")
186
+ ap.add_argument("query", help="natural-language query, e.g. 'which function escapes HTML'")
187
+ ap.add_argument("--index", default=DEFAULT_INDEX, help=f"index db (default {DEFAULT_INDEX})")
188
+ ap.add_argument("--caller-cell", default=DEFAULT_CALLER_CELL, help="A8 caller identity (default lab-ovh)")
189
+ ap.add_argument("--limit", type=int, default=8)
190
+ ap.add_argument("--json", action="store_true", help="machine-readable JSON output")
191
+ a = ap.parse_args(argv)
192
+ rows = structural_query(a.query, index_path=a.index, caller_cell=a.caller_cell, limit=a.limit)
193
+ print(json.dumps(rows, indent=2) if a.json else format_human(rows, a.query))
194
+ return 0
@@ -11,12 +11,14 @@ A host mounts it with a stdio server entry, e.g. in ``.mcp.json`` /
11
11
  {"mcpServers": {"swarph": {"command": "swarph",
12
12
  "args": ["mcp-server"]}}}
13
13
 
14
- Three tools are exposed, each backed by a plain, unit-testable helper so the
14
+ Four tools are exposed, each backed by a plain, unit-testable helper so the
15
15
  tool LOGIC is tested without spinning up the stdio transport:
16
16
 
17
- * ``swarph_search(query)`` → :func:`_search` — metaedge.surf semantic search.
18
- * ``swarph_add(uri)`` → :func:`_add` — trust-gated + scanned install.
19
- * ``swarph_describe(uri)`` → :func:`_describe` — parse a URI, no install.
17
+ * ``swarph_search(query)`` → :func:`_search` — metaedge.surf semantic search.
18
+ * ``swarph_add(uri)`` → :func:`_add` — trust-gated + scanned install.
19
+ * ``swarph_describe(uri)`` → :func:`_describe` — parse a URI, no install.
20
+ * ``swarph_codegraph_query(query)`` → :func:`_codegraph_query` — structural code
21
+ search (definitions + call sites) over the local codegraph index.
20
22
 
21
23
  The MCP Python SDK (``mcp``) is an OPTIONAL extra. :func:`run_mcp_server`
22
24
  fails gracefully with a ``pip install swarph-cli[mcp]`` hint when it's absent;
@@ -33,6 +35,7 @@ import urllib.error
33
35
  import urllib.request
34
36
 
35
37
  from swarph_cli.commands import add
38
+ from swarph_cli.commands import codegraph
36
39
 
37
40
  # --------------------------------------------------------------------------- #
38
41
  # Config
@@ -47,6 +50,12 @@ _DEFAULT_METAEDGE_URL = "https://metaedge.surf"
47
50
  #: of truth feeds every ``swarph_search`` call.
48
51
  _METAEDGE_URL = os.environ.get("SWARPH_METAEDGE_URL", _DEFAULT_METAEDGE_URL)
49
52
 
53
+ #: Module-level local codegraph index path the FastMCP tool wrapper reads.
54
+ #: Overridable via ``SWARPH_CODEGRAPH_INDEX`` (mirrors ``codegraph.DEFAULT_INDEX``).
55
+ _CODEGRAPH_INDEX = os.path.expanduser(
56
+ os.environ.get("SWARPH_CODEGRAPH_INDEX", "~/.swarph/codegraph/index.db")
57
+ )
58
+
50
59
 
51
60
  def _metaedge_token() -> str | None:
52
61
  """Optional bearer token for the search face (``SWARPH_METAEDGE_TOKEN``)."""
@@ -131,6 +140,46 @@ def _describe(uri: str) -> dict:
131
140
  }
132
141
 
133
142
 
143
+ def _codegraph_query(query: str, *, limit: int = 8) -> list[dict]:
144
+ """Structural code search over the local codegraph index (Task 1).
145
+
146
+ Delegates to :func:`codegraph.structural_query` against the module-level
147
+ ``_CODEGRAPH_INDEX`` path, using ``SWARPH_CELL`` (else
148
+ ``codegraph.DEFAULT_CALLER_CELL``) as the A8 caller identity and the
149
+ default operate-what-you-own allowlist (``allowlist=None``).
150
+
151
+ NEVER raises — any exception (missing index, corrupt db, bad query, or
152
+ anything else) is swallowed and returns ``[]`` so the MCP host always
153
+ gets a clean empty result rather than a transport error.
154
+
155
+ Returns COMPACT rows only — score/qualified_name/docstring are dropped
156
+ to keep the tool output tight for the calling agent.
157
+ """
158
+ try:
159
+ caller_cell = os.environ.get("SWARPH_CELL", codegraph.DEFAULT_CALLER_CELL)
160
+ rows = codegraph.structural_query(
161
+ query,
162
+ index_path=_CODEGRAPH_INDEX,
163
+ caller_cell=caller_cell,
164
+ limit=limit,
165
+ allowlist=None,
166
+ )
167
+ return [
168
+ {
169
+ "name": r["name"],
170
+ "kind": r["kind"],
171
+ "repo": r["repo"],
172
+ "file_path": r["file_path"],
173
+ "start_line": r["start_line"],
174
+ "signature": r["signature"],
175
+ "callers": r["callers"],
176
+ }
177
+ for r in rows
178
+ ]
179
+ except Exception:
180
+ return []
181
+
182
+
134
183
  # --------------------------------------------------------------------------- #
135
184
  # FastMCP server — thin @tool wrappers over the helpers above
136
185
  # --------------------------------------------------------------------------- #
@@ -160,6 +209,11 @@ try:
160
209
  """Parse a swarph:// URI and show what it points at, without installing."""
161
210
  return _describe(uri)
162
211
 
212
+ @mcp.tool()
213
+ def swarph_codegraph_query(query: str) -> list[dict]:
214
+ """Find where a function/class/method/symbol is DEFINED, or what CALLS it, across the indexed code repositories. Use this whenever you are reading, writing, debugging, or navigating code and need to locate a definition or trace call relationships by natural-language description (e.g. 'which function escapes HTML', 'where is the retry-parse helper defined', 'what calls the circuit breaker'). Returns ranked symbols with file path, line, signature, and caller count."""
215
+ return _codegraph_query(query)
216
+
163
217
  except ImportError: # pragma: no cover - exercised only when SDK absent
164
218
  mcp = None
165
219
 
@@ -183,7 +237,8 @@ def run_mcp_server(argv) -> int:
183
237
  prog="swarph mcp-server",
184
238
  description=(
185
239
  "Run an MCP (stdio) server exposing swarph_search / swarph_add / "
186
- "swarph_describe so any MCP host's AI gets the swarph toolbelt."
240
+ "swarph_describe / swarph_codegraph_query so any MCP host's AI "
241
+ "gets the swarph toolbelt."
187
242
  ),
188
243
  )
189
244
  parser.add_argument(
@@ -101,6 +101,7 @@ _VERB_HANDLERS: dict[str, str] = {
101
101
  "lane": "swarph_cli.commands.lane.run_lane",
102
102
  "install-multiplexer": "swarph_cli.commands.install_multiplexer.run_install_multiplexer",
103
103
  "highlight": "swarph_cli.commands.highlight.run_highlight",
104
+ "codegraph": "swarph_cli.commands.codegraph.run_codegraph",
104
105
  # Future: "list-peers", "list-adapters", etc.
105
106
  }
106
107
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.23.0
3
+ Version: 0.24.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,7 @@ This is one of three repos in the v0.3.x architecture:
72
72
  swarph "prompt" one-shot against any provider (claude/openai/gemini/deepseek/grok)
73
73
  swarph chat interactive REPL — multi-turn, slash commands, live provider switch
74
74
  swarph brain-ask "<q>" search the swarph-brain (gbrain) memory — $0 cited answer or raw chunks
75
+ swarph codegraph "<q>" structural code search — find where a symbol is defined / what calls it
75
76
  swarph brain serve run the gbrain HTTP brain server (the $0 semantic memory)
76
77
  swarph gateway serve run the bundled mesh-gateway server (the mesh's coordination/DM hub)
77
78
  swarph service serve stand up a $0 subscription-LLM HTTP lane (claude/codex/gemini)
@@ -117,6 +118,19 @@ $ swarph brain serve --gbrain-bin /opt/gbrain # explicit binary (else GBR
117
118
 
118
119
  Defaults are loopback bind (expose a tailnet IP explicitly) and a **1-year token TTL** — a short TTL silently 401s long-lived mesh cells, a lesson learned the hard way. If `gbrain` isn't on `PATH` the verb prints an install hint and exits `2`; `brain-ask` still queries any already-running brain.
119
120
 
121
+ ### `swarph codegraph` (v0.24.0)
122
+
123
+ Structural code search over a local **CodeGraph** index — the *what-where* hemisphere of the swarph brain. Ask in natural language where a function/class/symbol is **defined** or what **calls** it; get ranked hits with file:line, signature, and caller count.
124
+
125
+ ```bash
126
+ $ swarph codegraph "which function escapes special characters for HTML"
127
+ $ swarph codegraph "cron expression validator" --json --limit 5
128
+ ```
129
+
130
+ It queries a locally-built index at `~/.swarph/codegraph/index.db` (override with `--index` or `SWARPH_CODEGRAPH_INDEX`); the index is built out-of-band from your repos (tree-sitter → SQLite FTS5, BM25-ranked). If no index is present the verb simply returns no matches — it never touches the network and never reads code it wasn't pointed at. Results are scoped by an owner-allowlist gate (`--caller-cell` / `SWARPH_CELL`, default: your cell): public repos are always visible; private repos are visible to the owning cell.
131
+
132
+ The same capability is exposed as an **MCP tool** — running `swarph mcp-server` publishes `swarph_codegraph_query` alongside `swarph_search`/`swarph_add`/`swarph_describe`, so any MCP host's agent auto-discovers it and can reach for code-structure lookups while reading, writing, or debugging code. (Why a tool and not an always-on retrieval lane? Because *intent* to consult code structure lives with the calling agent — which already knows it's working on code — not in a similarity score.)
133
+
120
134
  ### `swarph gateway`
121
135
 
122
136
  The **mesh-gateway** is the coordination/DM server every other verb talks to — the peer registry, the DM inbox/outbox, feature aggregation with allowlist + caps, and lane/service control. It used to be a separate deployment; it's now bundled, so any host can stand one up with a single command. It pairs with the client verbs (`mesh`, `spawn`, `daemon`, `watchdog`) and with `brain-ask`/gbrain to form the mesh's coordination plane.
@@ -28,6 +28,7 @@ src/swarph_cli/commands/brain_ask.py
28
28
  src/swarph_cli/commands/cell.py
29
29
  src/swarph_cli/commands/channel.py
30
30
  src/swarph_cli/commands/chat.py
31
+ src/swarph_cli/commands/codegraph.py
31
32
  src/swarph_cli/commands/compress.py
32
33
  src/swarph_cli/commands/daemon.py
33
34
  src/swarph_cli/commands/event.py
@@ -94,6 +95,8 @@ tests/test_channel_command.py
94
95
  tests/test_chat_command.py
95
96
  tests/test_claude_parser.py
96
97
  tests/test_claude_tmux_template.py
98
+ tests/test_codegraph_command.py
99
+ tests/test_codegraph_mcp.py
97
100
  tests/test_compress_caller_convention.py
98
101
  tests/test_compress_command.py
99
102
  tests/test_compress_levers.py
@@ -25,3 +25,10 @@ def test_wrong_secret_rejected():
25
25
  def test_malformed_never_raises():
26
26
  for bad in ["", "nodot", "a.b.c", "!!.??", "x."]:
27
27
  assert verify_chain_token(SECRET, bad) is None
28
+
29
+ def test_non_str_token_fails_safe_not_raises():
30
+ # opaque tokens arrive from untrusted input (network / mesh DM); a non-str
31
+ # value (None, bytes, int) must fail safe to None, never AttributeError out
32
+ # of the caller. Regression for the gateway-copy fix that lagged the CLI.
33
+ for bad in [None, 123, b"a.b", ["a", "b"], {"chain_id": "x"}]:
34
+ assert verify_chain_token(SECRET, bad) is None
@@ -0,0 +1,107 @@
1
+ # tests/test_codegraph_command.py
2
+ """Tests for `swarph codegraph` — self-contained structural code search over a
3
+ LOCAL codegraph index. All fixture data is SYNTHETIC (invented repos/symbols) —
4
+ this module ships in the public swarph-cli package, so no real repo names,
5
+ symbol names, or fleet topology may appear here."""
6
+ import os
7
+ import sqlite3
8
+
9
+ import pytest
10
+
11
+ from swarph_cli.commands import codegraph as cg
12
+
13
+
14
+ def _tiny(tmp_path):
15
+ # synthetic fixture — NO real repo/symbol names
16
+ p = os.path.join(tmp_path, "i.db")
17
+ c = sqlite3.connect(p)
18
+ c.executescript(
19
+ "CREATE TABLE repos(name TEXT PRIMARY KEY, slug TEXT, path TEXT, visibility TEXT, indexed_at TEXT);"
20
+ "CREATE TABLE symbols(id INTEGER PRIMARY KEY, repo TEXT, name TEXT, kind TEXT, file_path TEXT, start_line INTEGER,"
21
+ " qualified_name TEXT, docstring TEXT, signature TEXT, name_search TEXT);"
22
+ "CREATE TABLE edges(src_symbol INTEGER, dst_symbol INTEGER, edge_type TEXT, repo TEXT);"
23
+ "CREATE VIRTUAL TABLE symbols_fts USING fts5(name_search, qualified_name, docstring, signature,"
24
+ " content='symbols', content_rowid='id', tokenize=\"porter unicode61 separators '_.'\");")
25
+ c.execute("INSERT INTO repos VALUES('alpha','o/alpha','/a','public','t')")
26
+ c.execute("INSERT INTO repos VALUES('beta','o/beta','/b','private','t')")
27
+ c.execute("INSERT INTO symbols(id,repo,name,kind,file_path,start_line,qualified_name,docstring,signature,name_search)"
28
+ " VALUES(1,'alpha','renderThing','function','ui.js',3,'ui.renderThing','renders the thing','function renderThing(x)','renderThing render thing')")
29
+ c.execute("INSERT INTO symbols(id,repo,name,kind,file_path,start_line,qualified_name,docstring,signature,name_search)"
30
+ " VALUES(2,'beta','secretOp','function','s.py',9,'b.secretOp','does a secret op','def secretOp(x)','secretOp secret op')")
31
+ c.execute("INSERT INTO symbols_fts(rowid,name_search,qualified_name,docstring,signature) SELECT id,name_search,qualified_name,docstring,signature FROM symbols")
32
+ c.commit()
33
+ c.close()
34
+ return p
35
+
36
+
37
+ def test_structural_query_finds_symbol_by_natural_language(tmp_path):
38
+ index_path = _tiny(tmp_path)
39
+ rows = cg.structural_query(
40
+ "which function renders the thing",
41
+ index_path=index_path, caller_cell="local", limit=8,
42
+ )
43
+ names = [r["name"] for r in rows]
44
+ assert "renderThing" in names
45
+
46
+
47
+ def test_structural_query_missing_index_returns_empty_list(tmp_path):
48
+ missing = os.path.join(tmp_path, "nope.db")
49
+ rows = cg.structural_query(
50
+ "renders the thing", index_path=missing, caller_cell="local", limit=8,
51
+ )
52
+ assert rows == []
53
+
54
+
55
+ def test_a8_default_allowlist_sees_private_repo(tmp_path):
56
+ # operate-what-you-own: local caller with default allowlist sees the
57
+ # private 'beta' symbol from its own index.
58
+ index_path = _tiny(tmp_path)
59
+ rows = cg.structural_query(
60
+ "secret op", index_path=index_path, caller_cell="local", limit=8,
61
+ )
62
+ names = [r["name"] for r in rows]
63
+ assert "secretOp" in names
64
+
65
+
66
+ def test_a8_empty_allowlist_hides_private_but_shows_public(tmp_path):
67
+ index_path = _tiny(tmp_path)
68
+ rows = cg.structural_query(
69
+ "secret op", index_path=index_path, caller_cell="someone-else",
70
+ limit=8, allowlist={},
71
+ )
72
+ names = [r["name"] for r in rows]
73
+ assert "secretOp" not in names
74
+
75
+ rows2 = cg.structural_query(
76
+ "renders the thing", index_path=index_path, caller_cell="someone-else",
77
+ limit=8, allowlist={},
78
+ )
79
+ names2 = [r["name"] for r in rows2]
80
+ assert "renderThing" in names2
81
+
82
+
83
+ def test_sanitize_query_empty_string(tmp_path):
84
+ assert cg._sanitize_query("") == ""
85
+
86
+
87
+ def test_format_human_no_matches():
88
+ assert "No structural matches" in cg.format_human([], "x")
89
+
90
+
91
+ def test_main_routes_codegraph_verb():
92
+ # `swarph codegraph "..."` dispatches through main._VERB_HANDLERS
93
+ from swarph_cli import main
94
+ assert main._VERB_HANDLERS["codegraph"] == "swarph_cli.commands.codegraph.run_codegraph"
95
+
96
+
97
+ def test_run_codegraph_cli_smoke(tmp_path, capsys):
98
+ # End-to-end argparse wiring: --index/--caller-cell/--json all reach
99
+ # structural_query and a JSON-decodable result comes back on stdout.
100
+ index_path = _tiny(tmp_path)
101
+ rc = cg.run_codegraph(
102
+ ["renders the thing", "--index", str(index_path), "--caller-cell", "local", "--json"]
103
+ )
104
+ assert rc == 0
105
+ import json
106
+ out = json.loads(capsys.readouterr().out)
107
+ assert any(r["name"] == "renderThing" for r in out)