#!/usr/bin/env python3 import argparse import getpass import json import os import platform import socket import sys from datetime import datetime, timezone from pathlib import Path SKIP_PARTS = {"node_modules", ".git", ".state", "logs", "references", "__pycache__"} def is_wsl(): try: release = Path("/proc/sys/kernel/osrelease").read_text(errors="ignore").lower() except Exception: release = "" return "microsoft" in release or "wsl" in release or "WSL_INTEROP" in os.environ def to_windows_path(path): text = str(path) if text.startswith("/mnt/") and len(text) >= 7 and text[5].isalpha() and text[6] == "/": drive = text[5].upper() rest = text[7:].replace("/", "\\") return drive + ":\\" + rest return None def read_bounded(path, limit=16384): try: data = path.read_bytes()[:limit] return data.decode("utf-8", errors="replace") except Exception: return "" def frontmatter_value(line): if ":" not in line: return None key, value = line.split(":", 1) return key.strip().lower(), value.strip().strip("\"'") def parse_skill_metadata(skill_md): text = read_bounded(skill_md) name = skill_md.parent.name description = "" lines = text.splitlines() if lines and lines[0].strip() == "---": for line in lines[1:]: if line.strip() == "---": break item = frontmatter_value(line) if item is None: continue key, value = item if key == "name" and value: name = value if key == "description" and value: description = value if not description: for line in lines: stripped = line.strip() if stripped and not stripped.startswith("---") and not stripped.startswith("#"): description = stripped break return name, description def iter_skill_files(root, max_depth): try: iterator = root.rglob("SKILL.md") for skill_md in iterator: try: rel = skill_md.relative_to(root) except ValueError: continue directory_parts = rel.parts[:-1] if len(directory_parts) == 0 or len(directory_parts) > max_depth: continue if any(part in SKIP_PARTS for part in directory_parts): continue yield skill_md except Exception as exc: raise RuntimeError(str(exc)) from exc def unique_paths(paths): seen = set() output = [] for raw in paths: path = Path(raw).expanduser() key = str(path) if key in seen: continue seen.add(key) output.append(path) return output def default_wsl_roots(): home = Path.home() roots = [home / ".agents" / "skills", home / ".codex" / "skills"] for raw in ("/root/.agents/skills", "/root/.codex/skills"): path = Path(raw) if str(path) not in {str(item) for item in roots}: roots.append(path) return roots def default_windows_roots(): if not is_wsl(): return [] users = Path("/mnt/c/Users") roots = [] try: children = list(users.iterdir()) if users.exists() else [] except Exception: children = [] for child in children: try: if not child.is_dir(): continue except Exception: continue lower = child.name.lower() if lower in {"all users", "default", "default user", "public"}: continue roots.append(child / ".agents" / "skills") roots.append(child / ".codex" / "skills") return roots def scan_root(scope, root, max_depth): try: root_exists = root.exists() root_error = None except Exception as exc: root_exists = False root_error = str(exc) record = { "scope": scope, "path": str(root), "windowsPath": to_windows_path(root), "exists": root_exists, "skillCount": 0, "error": root_error, } skills = [] if not record["exists"]: return record, skills try: for skill_md in iter_skill_files(root, max_depth): name, description = parse_skill_metadata(skill_md) skill = { "scope": scope, "name": name, "description": description, "path": str(skill_md.parent), "skillMd": str(skill_md), "windowsPath": to_windows_path(skill_md.parent), "root": str(root), } skills.append(skill) except Exception as exc: record["error"] = str(exc) record["skillCount"] = len(skills) return record, skills def main(): parser = argparse.ArgumentParser(description="discover WSL/Linux and Windows skill directories from a UniDesk ssh passthrough session") parser.add_argument("--scope", choices=["all", "wsl", "windows"], default="all", help="which skill roots to scan") parser.add_argument("--max-depth", type=int, default=4, help="maximum directory depth below each skill root") parser.add_argument("--limit", type=int, default=0, help="maximum skill rows to return; 0 means unlimited") parser.add_argument("--root", action="append", default=[], help="extra WSL/Linux skill root") parser.add_argument("--windows-root", action="append", default=[], help="extra Windows skill root, expressed as /mnt//...") args = parser.parse_args() if args.max_depth <= 0: print(json.dumps({"ok": False, "error": "--max-depth must be positive"}, ensure_ascii=False)) return 2 if args.limit < 0: print(json.dumps({"ok": False, "error": "--limit must be >= 0"}, ensure_ascii=False)) return 2 roots = [] if args.scope in ("all", "wsl"): roots.extend(("wsl", path) for path in default_wsl_roots()) roots.extend(("wsl", Path(raw).expanduser()) for raw in args.root) if args.scope in ("all", "windows"): roots.extend(("windows", path) for path in default_windows_roots()) roots.extend(("windows", Path(raw).expanduser()) for raw in args.windows_root) seen = set() unique = [] for scope, path in roots: key = (scope, str(path)) if key in seen: continue seen.add(key) unique.append((scope, path)) root_records = [] skills = [] for scope, root in unique: record, found = scan_root(scope, root, args.max_depth) root_records.append(record) skills.extend(found) scope_order = {"wsl": 0, "windows": 1} skills.sort(key=lambda item: (scope_order.get(str(item["scope"]), 9), str(item["name"]).lower(), str(item["path"]))) total_before_limit = len(skills) if args.limit > 0: skills = skills[:args.limit] counts = {"total": len(skills), "totalBeforeLimit": total_before_limit, "wsl": 0, "windows": 0} for skill in skills: scope = str(skill["scope"]) if scope in counts: counts[scope] += 1 payload = { "ok": True, "command": "unidesk ssh skills", "generatedAt": datetime.now(timezone.utc).isoformat(), "node": { "hostname": socket.gethostname(), "user": getpass.getuser(), "home": str(Path.home()), "platform": platform.platform(), "isWsl": is_wsl(), "python": sys.version.split()[0], }, "counts": counts, "roots": root_records, "skills": skills, } print(json.dumps(payload, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())