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.
- {swarph_cli-0.23.0/src/swarph_cli.egg-info → swarph_cli-0.24.0}/PKG-INFO +15 -1
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/README.md +14 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/pyproject.toml +1 -1
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/chain_token.py +8 -4
- swarph_cli-0.24.0/src/swarph_cli/commands/codegraph.py +194 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/mcp_server.py +60 -5
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/main.py +1 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0/src/swarph_cli.egg-info}/PKG-INFO +15 -1
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/SOURCES.txt +3 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_chain_token.py +7 -0
- swarph_cli-0.24.0/tests/test_codegraph_command.py +107 -0
- swarph_cli-0.24.0/tests/test_codegraph_mcp.py +98 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/LICENSE +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/setup.cfg +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_brain_ask_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_init_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_main.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_security.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_service_app.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_service_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.23.0 → swarph_cli-0.24.0}/tests/test_watchdog_model_rung.py +0 -0
- {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.
|
|
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.
|
|
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" }
|
|
@@ -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
|
|
36
|
-
# safe to None. NOT a bare `Exception`: an
|
|
37
|
-
# as a bug, not
|
|
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
|
-
|
|
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)``
|
|
18
|
-
* ``swarph_add(uri)``
|
|
19
|
-
* ``swarph_describe(uri)``
|
|
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
|
|
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.
|
|
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)
|