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