Add shared workspace skills for Cursor and Hermes.

Introduce gitnexus-search skill with REST CLI for remote GitNexus queries by repo name and prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-01 13:31:03 +00:00
commit fa48844fb3
3 changed files with 308 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Universal GitNexus search CLI for Cursor (fallback) and Hermes."""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
DEFAULT_SERVER = "https://gitnexus.mobile.agency-ai.dev"
REPO_ALIASES: dict[str, str] = {
"gateway": "one-gateway",
"one-gateway": "one-gateway",
"ai-lawyer": "ai-lawyer-srvr",
"ai-lawyer-srvr": "ai-lawyer-srvr",
}
MCP_PATHS: dict[str, str] = {
"one-gateway": "/data/gitnexus/repos/one-gateway",
"ai-lawyer-srvr": "/data/gitnexus/repos/ai-lawyer-srvr",
}
def base_url() -> str:
return os.environ.get("GITNEXUS_SERVER_URL", DEFAULT_SERVER).rstrip("/")
def normalize_repo(name: str) -> str:
key = name.strip().lower()
if key in REPO_ALIASES:
return REPO_ALIASES[key]
return name.strip()
def request(method: str, path: str, body: dict | None = None) -> object:
url = f"{base_url()}{path}"
headers = {"Accept": "application/json"}
data = None
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read().decode("utf-8")
if not raw.strip():
return {}
return json.loads(raw)
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
print(f"HTTP {exc.code} {exc.reason}: {detail}", file=sys.stderr)
raise SystemExit(1) from exc
except urllib.error.URLError as exc:
print(f"Request failed: {exc.reason}", file=sys.stderr)
raise SystemExit(1) from exc
def list_repos() -> list[dict]:
repos = request("GET", "/api/repos")
if not isinstance(repos, list):
raise SystemExit(f"Unexpected /api/repos response: {repos!r}")
return repos
def resolve_repo(name: str) -> dict:
canonical = normalize_repo(name)
matches = [r for r in list_repos() if r.get("name") == canonical]
if not matches:
available = sorted({r.get("name", "") for r in list_repos() if r.get("name")})
print(f"Unknown repo: {name!r} (normalized: {canonical!r})", file=sys.stderr)
print(f"Available: {', '.join(available) or '(none)'}", file=sys.stderr)
raise SystemExit(1)
with_files = [r for r in matches if (r.get("stats") or {}).get("files", 0) > 0]
if with_files:
return with_files[0]
if len(matches) == 1:
print(
f"WARN: repo {canonical!r} is indexed but has 0 files — re-index may be needed",
file=sys.stderr,
)
return matches[0]
print(f"Ambiguous repo {canonical!r}: multiple entries, none with files", file=sys.stderr)
for r in matches:
print(f" - path={r.get('path')}", file=sys.stderr)
raise SystemExit(1)
def search(repo_name: str, query: str, limit: int) -> dict:
repo_entry = resolve_repo(repo_name)
canonical = repo_entry.get("name", normalize_repo(repo_name))
payload = {"query": query, "repo": canonical, "limit": limit}
result = request("POST", "/api/search", payload)
if not isinstance(result, dict):
raise SystemExit(f"Unexpected /api/search response: {result!r}")
result["_repo"] = canonical
result["_repo_path"] = repo_entry.get("path", "")
result["_mcp_path"] = MCP_PATHS.get(canonical, repo_entry.get("path", ""))
return result
def format_text(result: dict) -> str:
lines = [
f"GitNexus search",
f"Server: {base_url()}",
f"Repo: {result.get('_repo')} ({result.get('_repo_path')})",
f"MCP path: {result.get('_mcp_path')}",
"",
]
results = result.get("results") or []
if not results:
lines.append("No results.")
return "\n".join(lines)
lines.append(f"Top {len(results)} hits:")
lines.append("")
for item in results:
rank = item.get("rank", "?")
path = item.get("filePath", "?")
score = item.get("score", 0)
nodes = item.get("nodeIds") or []
sym = ", ".join(n.split(":")[-1] for n in nodes[:3]) if nodes else "-"
lines.append(f" {rank}. {path} (score={score:.2f})")
if sym != "-":
lines.append(f" symbols: {sym}")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(
description="Search GitNexus remote index by repo name and query",
)
parser.add_argument("repo", help="Repository name or alias (e.g. one-gateway, ai-lawyer)")
parser.add_argument("query", nargs="+", help="Search prompt (words after repo)")
parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
parser.add_argument(
"--server",
default=None,
help="Override GITNEXUS_SERVER_URL",
)
args = parser.parse_args()
if args.server:
os.environ["GITNEXUS_SERVER_URL"] = args.server.rstrip("/")
query = " ".join(args.query).strip()
if not query:
print("Query cannot be empty", file=sys.stderr)
raise SystemExit(1)
result = search(args.repo, query, args.limit)
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()