Docs
Dashboard
Examples / Chat and researchBeta

GitHub profile summary

What a developer works with, measured instead of guessed: their public GitHub profile and repository list are read in a cloud browser, then their top repositories are cloned in a session's shell and measured there (lines of code per language, frameworks and libraries from the manifest files, CI, containers and infrastructure).

When to use it: You want what a developer works with, measured from their repositories, not guessed.

Usesextract over two pages, a shell session (exec, files)
Needsshell sessions and a model on the API (no keys of your own)
Sitegithub.com (public profile and repositories only)
Outputoutput/summary.md, output/result.json
LanguagesNode and Python

How it works:

  1. In a sandboxed browser: one extract call reads the profile and the repository list sorted by stars: name, bio, location, followers and each repository's name, address, description, language, stars and whether it is a fork.
  2. On the machine: stack.py shallow-clones the top TOP_REPOS of the user's own repositories (forks left out) and counts non-empty lines per language by file type, reads package.json, requirements.txt, pyproject.toml, go.mod, Cargo.toml, Gemfile and composer.json for well-known frameworks, and notes GitHub Actions, Docker, Terraform and Make. Nothing runs on your computer.

Use it on public profiles for things like knowing a project's maintainers better or summing up your own work; it reads what anyone can see and clones only public repositories.

The code

The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.

/** * GitHub profile summary: what a developer works with, measured, not guessed. Their public profile and repository * list are read in a sandboxed browser; their top repositories are cloned in the session's shell and measured there * (lines of code per language, frameworks from the manifests, CI and containers). * *   npx tsx node/index.ts            (BOXLINE_API_KEY; GITHUB_USER, TOP_REPOS; a plan with shell sessions) * * Public data only: the profile page, and repositories anyone can clone. Writes output/result.json and output/summary.md. */import { mkdirSync, readFileSync, writeFileSync } from "node:fs";import { dirname, join } from "node:path";import { fileURLToPath } from "node:url";import { Boxline } from "@boxline/sdk";const here = dirname(fileURLToPath(import.meta.url));const out = process.env.OUTPUT_DIR ?? "output";const user = process.env.GITHUB_USER ?? "octocat";const base = (process.env.GITHUB_URL ?? "https://github.com").replace(/\/$/, "");const topN = Number(process.env.TOP_REPOS ?? 3);const bx = new Boxline();type Repo = { name: string; url: string; description: string | null; language: string | null; stars: number; fork: boolean };type Profile = { name: string | null; bio: string | null; location: string | null; followers: number | null; repos: Repo[] };type Measured = { url: string; name: string; error?: string; linesByLanguage?: Record<string, number>; frameworks?: string[]; tools?: string[]; manifests?: string[] };// 1. The profile and its repositories, sorted by stars (two pages read in one call).const r = await bx.extract<Profile>({  urls: [`${base}/${user}`, `${base}/${user}?tab=repositories&type=source&sort=stargazers`],  prompt: `The GitHub user ${user}: their name, bio, location and followers from the profile, and the repositories listed (name, full https URL, description, main language, stars, whether it is a fork).`,  schema: {    type: "object",    properties: {      name: { type: ["string", "null"] },      bio: { type: ["string", "null"] },      location: { type: ["string", "null"] },      followers: { type: ["integer", "null"] },      repos: {        type: "array",        items: {          type: "object",          properties: { name: { type: "string" }, url: { type: "string" }, description: { type: ["string", "null"] }, language: { type: ["string", "null"] }, stars: { type: "integer" }, fork: { type: "boolean" } },          required: ["name", "url", "description", "language", "stars", "fork"],        },      },    },    required: ["name", "bio", "location", "followers", "repos"],  },});const profile = r.data;// Their own work: forks left out, most stars first; only addresses of this user's repositories are cloned.const own = profile.repos.filter((x) => !x.fork && x.url.startsWith(`${base}/${user}/`)).sort((a, b) => b.stars - a.stars);const top = own.slice(0, topN);console.log(`${profile.name ?? user}: ${profile.repos.length} repositories listed, ${own.length} of their own; measuring ${top.map((x) => x.name).join(", ")}`);if (!top.length) throw new Error(`no public repositories of ${user} to measure`);// 2. Clone and measure them on the machine (nothing runs on this computer).const session = await bx.sessions.create({ browser: false, shell: true, timeout: 600, idleTimeout: 300, userMetadata: { example: "github-profile-summary" } });console.log(`Session: ${session.id}`);let stack: { repos: Measured[]; linesByLanguage: Record<string, number> };try {  await session.files.write("stack.py", readFileSync(join(here, "../stack.py")));  const run = await session.exec("python stack.py", { env: { REPOS: top.map((x) => x.url).join(",") }, timeoutMs: 600_000 });  if (run.exitCode !== 0) throw new Error(`stack.py failed: ${run.stderr.trim()}`);  console.log(run.stdout.trim());  stack = JSON.parse(await session.files.readText("stack.json"));} finally {  await session.stop();}const total = Object.values(stack.linesByLanguage).reduce((a, b) => a + b, 0);const languages = Object.entries(stack.linesByLanguage).map(([language, lines]) => ({ language, lines, share: total ? Math.round((lines / total) * 1000) / 10 : 0 }));const frameworks = [...new Set(stack.repos.flatMap((x) => x.frameworks ?? []))].sort();const tools = [...new Set(stack.repos.flatMap((x) => x.tools ?? []))].sort();const repos = top.map((t) => ({ ...t, measured: stack.repos.find((m) => m.url === t.url) ?? null }));const md = [  `# ${profile.name ?? user} (${user})`,  "",  [profile.bio, profile.location, profile.followers !== null ? `${profile.followers} followers` : null].filter(Boolean).join(" · "),  "",  `## Languages (lines of code in ${repos.length} top repositories)`,  "",  ...(languages.length ? languages.map((l) => `- ${l.language}: ${l.lines} lines (${l.share}%)`) : ["- no code files"]),  "",  `**Frameworks and libraries:** ${frameworks.join(", ") || "none found in manifests"}`,  "",  `**Tools:** ${tools.join(", ") || "none found"}`,  "",  "## Repositories",  "",  ...repos.map((x) => `- [${x.name}](${x.url}) (${x.stars} stars): ${x.description ?? "no description"}${x.measured?.error ? ` (could not be cloned)` : ""}`),  "",].join("\n");mkdirSync(out, { recursive: true });writeFileSync(join(out, "summary.md"), md);writeFileSync(join(out, "result.json"), JSON.stringify({ user, profile: { ...profile, repos: undefined }, listed: profile.repos.length, languages, frameworks, tools, repos, usage: { modelUsd: r.usage.costUsd } }, null, 2));console.log(`\n${md}`);
stack.py
"""Runs in the session's shell: clones each repository in REPOS (comma-separated https URLs, shallow) and measures whatit is really made of: lines of code per language (by file type), frameworks and libraries from its manifest files,and tools (CI, containers, infrastructure). Writes /workspace/stack.json."""import jsonimport osimport reimport subprocessfrom collections import Counterfrom pathlib import PathLANG = {    ".py": "Python", ".ts": "TypeScript", ".tsx": "TypeScript", ".js": "JavaScript", ".jsx": "JavaScript", ".mjs": "JavaScript",    ".cjs": "JavaScript", ".go": "Go", ".rs": "Rust", ".java": "Java", ".kt": "Kotlin", ".rb": "Ruby", ".php": "PHP", ".c": "C",    ".h": "C", ".cc": "C++", ".cpp": "C++", ".hpp": "C++", ".cs": "C#", ".swift": "Swift", ".m": "Objective-C", ".scala": "Scala",    ".sh": "Shell", ".html": "HTML", ".css": "CSS", ".scss": "CSS", ".vue": "Vue", ".svelte": "Svelte", ".dart": "Dart",    ".ex": "Elixir", ".exs": "Elixir", ".lua": "Lua", ".r": "R", ".jl": "Julia", ".zig": "Zig", ".sql": "SQL",}# Libraries worth naming when a manifest lists them (the rest are counted, not named).KNOWN = {    "react", "next", "vue", "nuxt", "svelte", "@sveltejs/kit", "angular", "@angular/core", "express", "fastify", "koa", "nestjs",    "@nestjs/core", "electron", "vite", "webpack", "jest", "vitest", "mocha", "playwright", "@playwright/test", "puppeteer",    "tailwindcss", "prisma", "typeorm", "django", "flask", "fastapi", "pandas", "numpy", "torch", "tensorflow", "scikit-learn",    "pytest", "requests", "sqlalchemy", "pydantic", "rails", "sinatra", "rspec", "laravel/framework", "symfony/symfony",    "spring-boot", "gin-gonic/gin", "gorilla/mux", "tokio", "serde", "actix-web", "axum", "clap",}SKIP = {".git", "node_modules", "vendor", "dist", "build", ".venv", "venv", "__pycache__", "target"}def files(root):    for dirpath, dirnames, filenames in os.walk(root):        dirnames[:] = [d for d in dirnames if d not in SKIP]        for f in filenames:            yield Path(dirpath) / fdef deps(path):    name, text = path.name, path.read_text(errors="ignore")    if name == "package.json":        try:            j = json.loads(text)            return [*j.get("dependencies", {}), *j.get("devDependencies", {})]        except ValueError:            return []    if name in ("requirements.txt", "requirements-dev.txt"):        return [re.split(r"[<>=~!\[ ;]", line.strip())[0].lower() for line in text.splitlines() if line.strip() and not line.startswith(("#", "-"))]    if name == "pyproject.toml":        return [m.lower() for m in re.findall(r'^\s*"?([A-Za-z0-9_.-]+)\s*[<>=~!"\[]', text, re.M)]    if name == "go.mod":        return [m.split("/", 1)[1] if m.startswith("github.com/") else m for m in re.findall(r"^\s*(?:require\s+)?([\w.\-/]+)\s+v\d", text, re.M)]    if name == "Cargo.toml":        block = text.split("[dependencies]", 1)[1].split("\n[", 1)[0] if "[dependencies]" in text else ""        return re.findall(r"^\s*([A-Za-z0-9_-]+)\s*=", block, re.M)    if name == "Gemfile":        return re.findall(r"""^\s*gem\s+['"]([^'"]+)""", text, re.M)    if name == "composer.json":        try:            return list(json.loads(text).get("require", {}))        except ValueError:            return []    return []repos = []for url in [u.strip() for u in os.environ.get("REPOS", "").split(",") if u.strip()]:    name = url.rstrip("/").split("/")[-1].removesuffix(".git")    dest = Path("/workspace/repos") / name    r = subprocess.run(["git", "clone", "--depth", "1", "--quiet", url, str(dest)], capture_output=True, text=True, timeout=180)    if r.returncode != 0:        repos.append({"url": url, "name": name, "error": r.stderr.strip()[-300:]})        continue    lines, libs, tools, manifests = Counter(), set(), set(), []    for f in files(dest):        rel = f.relative_to(dest)        lang = LANG.get(f.suffix.lower())        if lang and f.stat().st_size < 2_000_000:            lines[lang] += sum(1 for line in f.open(errors="ignore") if line.strip())        if f.name in ("package.json", "requirements.txt", "requirements-dev.txt", "pyproject.toml", "go.mod", "Cargo.toml", "Gemfile", "composer.json") and "node_modules" not in rel.parts:            manifests.append(str(rel))            libs |= {d for d in deps(f) if d.lower() in KNOWN}        if rel.parts[:2] == (".github", "workflows"):            tools.add("GitHub Actions")        if f.name in ("Dockerfile", "docker-compose.yml", "compose.yaml"):            tools.add("Docker")        if f.suffix == ".tf":            tools.add("Terraform")        if f.name == "Makefile":            tools.add("Make")    repos.append({"url": url, "name": name, "linesByLanguage": dict(lines.most_common()), "frameworks": sorted(libs), "tools": sorted(tools), "manifests": manifests})total = Counter()for r in repos:    total.update(r.get("linesByLanguage", {}))json.dump({"repos": repos, "linesByLanguage": dict(total.most_common())}, open("/workspace/stack.json", "w"), indent=1)for r in repos:    print(f"{r['name']}: {r.get('error') or ', '.join(f'{k} {v}' for k, v in list(r['linesByLanguage'].items())[:4]) or 'no code files'}")

Inputs

Environment variables the example reads:

  • GITHUB_USER (default octocat), TOP_REPOS (default 3).

Run it

Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/github-profile-summary. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.

# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=…   # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts

Then check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.

What its check proves

check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.

Every measured repository is the user's own (not a fork), at least one was cloned, lines of code were counted and the language shares add up to 100%. For octocat: HTML and CSS, as their top repositories are web pages.

Source

github.com/Boxline-dev/examples/github-profile-summary on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.