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
+62
View File
@@ -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
```
+82
View File
@@ -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 510 trafień: rank, filePath, score, kluczowe symbole/nodeIds
3. **Synteza:** 12 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)
+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()