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:
@@ -0,0 +1,62 @@
|
||||
# Workspace skills (`/home/test-user/cursor/skills`)
|
||||
|
||||
Wspólne skille dla **Cursor** i **Hermes** w ekosystemie agency-ai.
|
||||
|
||||
## Skille
|
||||
|
||||
| Skill | Wywołanie | Opis |
|
||||
|-------|-----------|------|
|
||||
| [gitnexus-search](gitnexus-search/SKILL.md) | `gitnexus-search <repo> <prompt...>` | Przeszukiwanie zdalnego GitNexus po nazwie repo |
|
||||
|
||||
## Cursor
|
||||
|
||||
Skills w tym katalogu nie są automatycznie widoczne w Cursor — utwórz symlink:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.cursor/skills
|
||||
ln -sfn /home/test-user/cursor/skills/gitnexus-search ~/.cursor/skills/gitnexus-search
|
||||
```
|
||||
|
||||
Opcjonalnie cały katalog:
|
||||
|
||||
```bash
|
||||
ln -sfn /home/test-user/cursor/skills ~/.cursor/skills-workspace
|
||||
```
|
||||
|
||||
Wymagany MCP GitNexus w `~/.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitnexus": {
|
||||
"url": "https://gitnexus.mobile.agency-ai.dev/api/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Po symlinku: **Reload Window** + nowa sesja chatu.
|
||||
|
||||
## Hermes (m3gan)
|
||||
|
||||
W promptcie systemowym lub `INSTALL-AGENT.md` podaj ścieżkę bezwzględną do skilla:
|
||||
|
||||
```text
|
||||
/home/test-user/cursor/skills/gitnexus-search/SKILL.md
|
||||
```
|
||||
|
||||
Hermes używa REST (skrypt), gdy MCP niedostępny:
|
||||
|
||||
```bash
|
||||
python3 /home/test-user/cursor/skills/gitnexus-search/scripts/gitnexus-search.py \
|
||||
one-gateway "deploy_runner"
|
||||
```
|
||||
|
||||
Więcej: [one-gateway/docs/hermes/GITNEXUS-CLIENT.md](../one-gateway/docs/hermes/GITNEXUS-CLIENT.md).
|
||||
|
||||
## Przykład
|
||||
|
||||
```text
|
||||
gitnexus-search ai-lawyer-srvr routes_paperless admin
|
||||
gitnexus-search gateway gitnexus_workspace_dir
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: gitnexus-search
|
||||
description: >-
|
||||
Przeszukuje zdalny GitNexus po nazwie repo i promptcie użytkownika.
|
||||
Użyj gdy użytkownik wywołuje skill gitnexus-search, @gitnexus-search,
|
||||
lub podaje repo + zapytanie do grafu kodu (search, symbole, flow).
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# gitnexus-search
|
||||
|
||||
Jawne wyszukiwanie w zdalnym GitNexus (`https://gitnexus.mobile.agency-ai.dev`). Działa w **Cursor** (MCP) i **Hermes** (REST).
|
||||
|
||||
## Format wejścia
|
||||
|
||||
```text
|
||||
gitnexus-search <repo> <prompt...>
|
||||
```
|
||||
|
||||
- Pierwszy token po nazwie skilla = **repo**
|
||||
- **Cała reszta linii** = prompt wyszukiwania (może zawierać spacje)
|
||||
|
||||
Przykłady:
|
||||
|
||||
```text
|
||||
gitnexus-search one-gateway gitnexus_workspace_dir
|
||||
gitnexus-search ai-lawyer-srvr routes_paperless admin flow
|
||||
gitnexus-search gateway deploy_runner install path
|
||||
```
|
||||
|
||||
## Aliasy repo
|
||||
|
||||
| Użytkownik wpisuje | Repo GitNexus | Ścieżka MCP (przy konflikcie) |
|
||||
|--------------------|---------------|-------------------------------|
|
||||
| `one-gateway`, `gateway` | `one-gateway` | `/data/gitnexus/repos/one-gateway` |
|
||||
| `ai-lawyer-srvr`, `ai-lawyer` | `ai-lawyer-srvr` | `/data/gitnexus/repos/ai-lawyer-srvr` |
|
||||
|
||||
Nieznane repo → `list_repos` / `GET /api/repos` i pokaż dostępne nazwy.
|
||||
|
||||
## Wykonanie — Cursor (preferowane)
|
||||
|
||||
Gdy MCP `gitnexus` jest Connected:
|
||||
|
||||
1. `list_repos` — potwierdź, że repo istnieje i ma `stats.files > 0`
|
||||
2. `query` z argumentami:
|
||||
- `search_query`: prompt użytkownika
|
||||
- `repo`: znormalizowana nazwa (lub ścieżka bezwzględna z tabeli)
|
||||
- `limit`: 10
|
||||
3. Jeśli prompt sugeruje impact/zależności — opcjonalnie jeden `context` lub `impact` na top symbolu z wyników
|
||||
|
||||
## Wykonanie — Hermes / fallback
|
||||
|
||||
Gdy MCP niedostępny — **od razu** REST, bez pytania użytkownika:
|
||||
|
||||
```bash
|
||||
python3 /home/test-user/cursor/skills/gitnexus-search/scripts/gitnexus-search.py \
|
||||
<repo> "<prompt>"
|
||||
```
|
||||
|
||||
Opcjonalnie: `--limit N`, `--json`.
|
||||
|
||||
## Format odpowiedzi
|
||||
|
||||
1. **Nagłówek:** repo + prompt (echo)
|
||||
2. **Tabela** top 5–10 trafień: rank, filePath, score, kluczowe symbole/nodeIds
|
||||
3. **Synteza:** 1–2 zdania — co wyniki sugerują w architekturze
|
||||
4. **Następny krok:** `context`/`impact` (MCP), otwarcie pliku lokalnie, lub doprecyzowanie promptu
|
||||
|
||||
## Błędy
|
||||
|
||||
| Sytuacja | Akcja |
|
||||
|----------|-------|
|
||||
| Repo nieznane | Lista z `/api/repos` |
|
||||
| Indeks pusty (`files: 0`) | Wskazówka: re-index (`gitnexus-index-remote.py` w projekcie) |
|
||||
| MCP Error / brak narzędzi | REST przez `gitnexus-search.py` |
|
||||
| HTTP / timeout | `curl -fsS $GITNEXUS_SERVER_URL/api/health` |
|
||||
|
||||
## Powiązane
|
||||
|
||||
- Skrypt: [`scripts/gitnexus-search.py`](scripts/gitnexus-search.py)
|
||||
- Hermes: [`one-gateway/docs/hermes/GITNEXUS-CLIENT.md`](../../one-gateway/docs/hermes/GITNEXUS-CLIENT.md)
|
||||
- Workspace skills: [`../README.md`](../README.md)
|
||||
Executable
+164
@@ -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()
|
||||
Reference in New Issue
Block a user