Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .cursor/rules/repo-context.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description: Shared repo context
alwaysApply: true
---

Read `AGENTS.md` first. Treat it as the source of truth for this repo's
commands, invariants, public naming, release constraints, and first-user path.

This file is a thin client adapter for Cursor. Do not add independent strategy,
roadmap, package-name, release, or baseline-scope rules here.
8 changes: 8 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Agent context

Read `AGENTS.md` first. Treat it as the source of truth for this repo's
commands, invariants, public naming, release constraints, and first-user path.

This file is a thin client adapter for GitHub Copilot and VS Code. Do not add
independent strategy, roadmap, package-name, release, or baseline-scope rules
here.
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,48 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_call:

# Least-privilege: this workflow only reads the repo and runs tests.
permissions:
contents: read

jobs:
static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: secret-scanning with gitleaks
env:
GITLEAKS_VERSION: 8.30.1
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
run: |
set -euo pipefail
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o /tmp/gitleaks.tgz
echo "${GITLEAKS_SHA256} /tmp/gitleaks.tgz" | sha256sum -c -
tar xz -C /tmp -f /tmp/gitleaks.tgz
/tmp/gitleaks detect --source . --verbose --redact

- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Check licenses
run: npm exec -- license-checker-evergreen --failOn "GPL-2.0;GPL-3.0;AGPL-3.0"

- name: Security audit
run: npm audit --audit-level=high

- name: Syntax check
run: npm run check

test:
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -89,3 +125,18 @@ jobs:

docker logs pkit
docker rm -f pkit

ci:
needs: [static-checks, test, docker]
if: always()
runs-on: ubuntu-latest
steps:
- name: Aggregate result
run: |
if [ "${{ needs.static-checks.result }}" != "success" ] || \
[ "${{ needs.test.result }}" != "success" ] || \
[ "${{ needs.docker.result }}" != "success" ]; then
echo "::error::CI failed (static-checks=${{ needs.static-checks.result }}, test=${{ needs.test.result }}, docker=${{ needs.docker.result }})"
exit 1
fi
echo "All checks passed."
38 changes: 38 additions & 0 deletions .github/workflows/claude-security-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: AI Security Review

on:
pull_request:

permissions:
pull-requests: write
contents: read

jobs:
ai-review:
runs-on: ubuntu-latest
steps:
- name: Check CLAUDE_API_KEY presence
id: gate
env:
API_KEY: ${{ secrets.CLAUDE_API_KEY }}
run: |
set -euo pipefail
if [ -z "${API_KEY:-}" ]; then
echo "::notice::CLAUDE_API_KEY secret is not set; skipping AI security review. Set the secret in repo settings to enable."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- uses: actions/checkout@v6
if: steps.gate.outputs.skip == 'false'
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 2

- name: AI security review
if: steps.gate.outputs.skip == 'false'
uses: anthropics/claude-code-security-review@0c6a49f1fa56a1d472575da86a94dbc1edb78eda
with:
comment-pr: true
claude-api-key: ${{ secrets.CLAUDE_API_KEY }}
31 changes: 31 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# CodeQL advanced setup keeps the static-analysis policy visible in the repo.
name: CodeQL

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'

permissions:
contents: read
security-events: write

jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: javascript-typescript

- name: Autobuild
uses: github/codeql-action/autobuild@v4

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
76 changes: 76 additions & 0 deletions .github/workflows/maintenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Maintenance

on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
ci:
uses: ./.github/workflows/ci.yml

open-issue-on-failure:
needs: ci
if: failure()
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'maintenance',
});
if (existing.data.length > 0) return;
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Maintenance CI failed',
body: `The scheduled maintenance CI check failed.\n\n**Run:** ${run.data.html_url}`,
labels: ['maintenance'],
});

close-issue-on-success:
needs: ci
if: success()
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const open = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'maintenance',
});
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
for (const issue of open.data) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Maintenance CI is green again.\n\n**Run:** ${run.data.html_url}`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
});
}
29 changes: 29 additions & 0 deletions .github/workflows/stale.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Stale Issues

on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

permissions:
issues: write
pull-requests: write

jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
days-before-stale: 30
days-before-close: 7
stale-issue-label: stale
stale-pr-label: stale
stale-issue-message: >
This issue has been inactive for 30 days and will be closed in 7 days
unless there is new activity.
stale-pr-message: >
This PR has been inactive for 30 days and will be closed in 7 days
unless there is new activity.
exempt-issue-labels: pinned,bug,help wanted
exempt-pr-labels: pinned,work in progress
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ProfileKit

Composable SVG card endpoints for GitHub profiles, READMEs, blogs, and personal
sites. Vanilla Node, no runtime dependencies, Vercel API routes plus Docker
self-hosting.

## Run this repo

```bash
npm run check
npm test
docker build -t profilekit:local .
docker run --rm -p 3000:3000 profilekit:local
```

## Structure

```
api/
health.js -> health endpoint
divider.js -> SVG divider endpoint
src/
cards/ -> card renderers
core/ -> shared SVG/query helpers
server.js -> local/self-host HTTP adapter
tests/ -> node:test coverage for endpoints and renderers
```

## Invariants

- Keep endpoints deterministic: same query string means same SVG.
- Treat every query parameter as untrusted input. Clamp numbers, whitelist
enum-like strings, and escape text before embedding in SVG.
- Do not add user ranking, scoring, ratings, or analytics capture.
- Do not introduce a database or server-side persistence for card rendering.
- Docker and Vercel paths must stay behaviorally equivalent.
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Agent context

Read `AGENTS.md` first. Treat it as the source of truth for this repo's
commands, invariants, public naming, release constraints, and first-user path.

This file is a thin client adapter. Do not add independent strategy, roadmap,
package-name, release, or baseline-scope rules here.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ A community gallery for sharing single-card presets and adopting others' designs

## About this project

**Currently implemented.** 28 SVG card endpoints (`/api/*`), 17 built-in themes plus gist-hosted custom palettes via `theme_url=`, five bundled variable fonts, `/api/stack` composition with namespaced child IDs, a live playground at [profilekit.vercel.app](https://profilekit.vercel.app), and an MCP wrapper at [`@heznpc/profilekit-mcp`](https://www.npmjs.com/package/@heznpc/profilekit-mcp). Two deployment paths: **Vercel functions** (primary, `api/[endpoint].js`) and an **optional self-hosted Docker** image (`Dockerfile` + `server.js`) running the same handlers. Zero runtime dependencies, 30-minute CDN cache on the hosted instance.
**Currently implemented.** 28 SVG card endpoints (`/api/*`), 17 built-in themes plus gist-hosted custom palettes via `theme_url=`, five bundled variable fonts, `/api/stack` composition with namespaced child IDs, a live playground at [profilekit.vercel.app](https://profilekit.vercel.app), and an MCP wrapper at [`profilekit-mcp`](https://www.npmjs.com/package/profilekit-mcp). Two deployment paths: **Vercel functions** (primary, `api/[endpoint].js`) and an **optional self-hosted Docker** image (`Dockerfile` + `server.js`) running the same handlers. Zero runtime dependencies, 30-minute CDN cache on the hosted instance.

**Planned.** A single-card preset gallery at `/gallery` — adopt someone else's design URL as a starting point, then tweak parameters in the editor. Cross-agent preset compile (one preset → Claude Code, Cursor, Codex CLI configs).

Expand Down Expand Up @@ -182,7 +182,7 @@ The GitHub-profile-card space has two long-running projects ProfileKit is most o
| Configuration | Query string + optional `theme_url=` gist for palettes | Query string + per-theme presets | YAML in `.github/workflows/` |
| Runtime deps | Zero (Node 22 `node:test`, `node:fetch`) | Several | Action toolchain + Docker image |
| Cards beyond GitHub stats | Hero / section / divider / now / timeline / tags / toc / typing / wave / terminal / neon / glitch / matrix / snake / equalizer / heartbeat / constellation / radar / quote / posts (devto, medium, rss) | GitHub stats, languages, pin, gists | Mostly GitHub stats; plugin set is the largest of the three |
| MCP integration | First-class — [`@heznpc/profilekit-mcp`](https://www.npmjs.com/package/@heznpc/profilekit-mcp) lets Claude / Cursor / Codex CLI build cards as a tool call | None | None |
| MCP integration | First-class — [`profilekit-mcp`](https://www.npmjs.com/package/profilekit-mcp) lets Claude / Cursor / Codex CLI build cards as a tool call | None | None |
| Composition into one image | `/api/stack?cards=hero,section,now,…` | Not native | The whole point of metrics is a single composed image |

**When ProfileKit fits well**: you want the same card definition usable in a GitHub README *and* a dev.to bio *and* an MCP tool call, you don't want a GitHub Action committing to your repo, and "no ranking, composable presentation" sounds right for your profile.
Expand Down Expand Up @@ -709,7 +709,7 @@ The Docker path is purely additive — the Vercel path keeps working unchanged.

## Roadmap

- **Now** — 28 card endpoints, 17 themes, playground at [profilekit.vercel.app](https://profilekit.vercel.app), MCP server at [`@heznpc/profilekit-mcp`](https://www.npmjs.com/package/@heznpc/profilekit-mcp), curated picks in the Templates tab.
- **Now** — 28 card endpoints, 17 themes, playground at [profilekit.vercel.app](https://profilekit.vercel.app), MCP server at [`profilekit-mcp`](https://www.npmjs.com/package/profilekit-mcp), curated picks in the Templates tab.
- **Next — Gallery.** Single-card presets shareable by URL or registered in a browseable index at `profilekit.vercel.app/gallery`. Adopting a preset opens it in the editor with the original params pre-filled so you tweak from a starting point instead of a blank canvas. Editing UX targets a Sims 3 Create-A-Style / Naver-blog-editor feel — direct manipulation on the preview, not just a form. **Explicit non-goals**: no ratings, no rankings, no remix lineage, no leaderboards.
- **Parallel** — Cross-agent compile (one preset definition → Claude Code, Cursor, Codex CLI configs). Lives as a feature, not a roadmap tier.

Expand Down
25 changes: 25 additions & 0 deletions claude-security-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Security guidance

This file is read by Anthropic's Claude Code Security Guidance Plugin as an
in-session guard while Claude writes code. It complements the
`claude-code-security-review` GitHub Action and the repo-level
`audit_security` check.

## Universal rules

- Never log secrets, tokens, cookies, or raw request headers.
- Never use `eval`, `Function()`, or shell execution for request data.
- Validate and clamp all query parameters before rendering SVG.
- Escape user-visible text before embedding it in SVG or HTML.
- Keep `.env*` files out of git. Use examples with placeholder values only.
- Use OIDC trusted publishing where publishing is added; do not add long-lived
registry tokens to workflows.

## ProfileKit rules

- SVG endpoints must be deterministic and side-effect free.
- Do not add persistence, analytics capture, tracking pixels, or ranking data
to card rendering.
- Maintain parity between Vercel API routes and the Docker/self-host server.
- Cap width, height, text length, color strings, and list sizes so malformed
query strings cannot produce excessive SVG output.
Loading
Loading