Agent skill

Assembly Solver

by andymai in andymai/brepjs

This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLYNOTCONVERGED / ASSEMBLYMATEINVALID / ASSEMBLYSOLVEFAILED…

Apache-2.0Auto-check passedDevelopment

Install Assembly Solver

skills CLI
$ npx skills add andymai/brepjs --skill assembly-solver -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install andymai/brepjs assembly-solver --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/andymai/brepjs.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/assembly-solver .claude/skills/assembly-solver && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
assembly-solver
GitHub stars
114
Token cost
~3.7k tokens
SKILL.md length
1,407 words
Files
1
Skills in repo
21
Repo updated
First seen
Licence
Apache-2.0

At a glance

This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLYNOTCONVERGED / ASSEMBLYMATEINVALID / ASSEMBLYSOLVEFAILED…

  • Works in 2 steps: Assembly tree + mates + joints — an… → Assembly STEP export —…
  • Phrases include assembly wont converge
  • SKILL.md covers Two unrelated meanings of…, The solver mental model, From MateEntity to a solver… and Supported constraint pairs, plus 9 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md

What it does

Assembly Solver is an agent skill from andymai/brepjs. This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLYNOTCONVERGED / ASSEMBLYMATEINVALID / ASSEMBLYSOLVEFAILED, adding or extending a mate or solver entity-pair, adding a joint type, or driving a mechanism. Trigger phrases include "assembly won't converge", "Unsupported constraint types", "solveAssembly returns Err", "add a mate/joint/constraint type", "extend TRANSLATIONALPAIRS", "solveMate: unsupported entity pair escaped filter"…

Its SKILL.md is about 3.7k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Development, covering Debugging. The repository describes itself as: Web CAD library with exact B-Rep geometry. The licence is Apache-2.0.

When your agent uses it

  • Phrases include assembly wont converge
  • Unsupported constraint types
  • SolveAssembly returns Err
  • Add a mate/joint/constraint type

Example prompts

  • “assembly won”
  • “Unsupported constraint types”
  • “solveAssembly returns Err”
  • “/assembly-solver”

Workflow steps

2 steps, taken from the first numbered list in SKILL.md.

  1. Assembly tree + mates + joints — an immutable AssemblyNode tree (src/operations/assemblyFns.ts), constraint mates…
  2. Assembly STEP export — exportAssemblySTEP / createAssembly / exportSTEP (src/operations/exporterFns.ts, src/operations/exporters.ts). An…

What it can do on your machine

Read from SKILL.md and the folder at commit ee50994. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md.

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Assembly Solver loads about 3.7k tokens when it runs. Until then it costs about 205 tokens; SKILL.md has 1,407 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~205
When it runs · the whole SKILL.md, loaded when a task matches
~3.7k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from andymai/brepjs at commit ee50994, republished under its Apache-2.0 licence (© andymai). 1,407 words, ~3,744 tokens.

Download SKILL.mdSave it as .claude/skills/assembly-solver/SKILL.md (or your agent's skills folder).
name
assembly-solver
description
This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLY_NOT_CONVERGED / ASSEMBLY_MATE_INVALID / ASSEMBLY_SOLVE_FAILED, adding or extending a mate or solver entity-pair, adding a joint type, or driving a mechanism. Trigger phrases include "assembly won't converge", "Unsupported constraint types", "solveAssembly returns Err", "add a mate/joint/constraint type", "extend TRANSLATIONAL_PAIRS", "solveMate: unsupported entity pair escaped filter", "revolute/prismatic/cylindrical/planar/spherical joint", "forwardKinematics", "inverse kinematics / IK target", "DH table / Denavit-Hartenberg", "export/import URDF", or "positioned STEP export from an assembly". Not for boolean fuse/cut compounds (that is debugging-geometry).

Assemblies, constraint solving, and kinematic joints

Covers two Layer-2 subsystems: the immutable assembly tree with its analytical constraint solver, and drivable kinematic joints (forward/inverse kinematics, DH, URDF). All of it is pure data + math with no kernel calls, except the STEP export at the very end.

Two unrelated meanings of "assembly" — do not conflate

This is the number-one source of confusion. There are two "assembly" APIs that share a word and nothing else.

  1. Assembly tree + mates + joints — an immutable AssemblyNode tree (src/operations/assemblyFns.ts), constraint mates (src/operations/mateFns.ts), and kinematic joints (src/operations/jointFns.ts). Pure data + math. This is what the constraint solver and all the kinematics operate on.
  2. Assembly STEP export — exportAssemblySTEP / createAssembly / exportSTEP (src/operations/exporterFns.ts, src/operations/exporters.ts). An XCAF colored/named multi-shape STEP writer. It takes a flat ShapeOptions[], not an AssemblyNode, and never reads mates, joints, or solved transforms.

There is no bridge between them. A solved tree's transforms are not fed into exportAssemblySTEP. To write a positioned STEP, apply the solved transforms to the shapes and pass them as ShapeOptions[].

The solver mental model

Read CLAUDE.md's "Assembly solver composes constraints down a chain" gotcha first — it is the authoritative one-liner. solveConstraints(nodes, constraints) in src/kernel/solverAdapter.ts is the analytical (closed-form, non-iterative) core. Its algorithm:

  • Every node starts at origin/identity.
  • A positioning mate is one of coincident, distance, angle, concentric (the POSITIONING_TYPES set) with both entities present. entityA is the reference, entityB is the dependent.
  • Anchors sit at the origin: any node that is never a dependent (a chain root), plus any explicit fixed node.
  • Well-typed mates resolve in topological rounds: a mate places its dependent once its reference is already placed, solving against the reference's solved world-space pose (rotation included), so multi-body chains compose.
  • Diagnostics: an entity-type mismatch is pushed as type(a-b) and dropped; a mate whose reference never resolves (cycle/dangling) is pushed as type(unanchored). converged = unsupported.length === 0.

mateFns.solveAssembly wraps this: it extracts geometry, calls solveConstraints, and maps the result to a Result<AssemblySolveResult>.

From MateEntity to a solver entity type

A MateEntity is { node, face?, edge?, point? }. extractEntity in mateFns.ts maps it to a solver entity typed plane, axis, or point:

  • face → faceAxis(face) (from faceFns.ts); non-null (a cylindrical/axial face) → axis, else plane via faceCenter + normalAt. faceAxis returning null is the axial-vs-planar discriminator.
  • edge → LINE type → axis along its tangent; else curveAxis (circular edge, e.g. a bore rim) → axis; else null.
  • point → point.

If extraction returns null for either entity, solveAssembly fails with ASSEMBLY_MATE_INVALID ("could not extract geometry from mate entities").

Supported constraint pairs

isSupportedPair (in solverAdapter.ts) decides solvability:

Mate typeAllowed (entityA-entityB) pairs
coincident, distanceTRANSLATIONAL_PAIRS: plane-plane, plane-point, point-plane, point-point, axis-axis, axis-point, point-axis
concentricREQUIRED_ENTITIES: axis-axis only
angleREQUIRED_ENTITIES: plane-plane only
fixedone entity, anchors that node at origin

Both orders are listed for translational pairs, so entities need not be pre-ordered.

Debugging a failed solve

The three error codes live in src/core/errors.ts (ASSEMBLY_MATE_INVALID, ASSEMBLY_SOLVE_FAILED, ASSEMBLY_NOT_CONVERGED). URDF errors instead use the generic VALIDATION_FAILED; STEP export uses STEP_EXPORT_FAILED.

Symptom → cause → fix

SymptomCauseFix
ASSEMBLY_MATE_INVALID: no mates definedCalled solveAssembly before addMateAdd at least one mate
ASSEMBLY_MATE_INVALID: could not extract geometryA MateEntity face/edge yielded no plane/axis/point (e.g. a non-line, non-circular edge)Pick an axial/planar face, a straight or circular edge, or a point
ASSEMBLY_NOT_CONVERGED: Unsupported constraint types: coincident(axis-plane) …Entity-type mismatch — pair not in the supported table (the canonical case is coincident(axis-plane))Fix the entities so the pair is supported (e.g. axis-axis for concentric, plane-plane for angle)
ASSEMBLY_NOT_CONVERGED: … concentric(plane-plane) …concentric/angle got the wrong entity types (they require axis-axis / plane-plane)Feed the required entity types
ASSEMBLY_NOT_CONVERGED: … coincident(unanchored) …Reference never resolved — a mutual-reference cycle (a→b, b→a) or a dangling reference, with no root/fixed anchorAdd a fixed mate to anchor the chain, or break the loop
ASSEMBLY_NOT_CONVERGED: … solver did not converge (no unsupported list)Rare: no diagnostics but still not convergedInspect the mate graph for an unanchored dependent
ASSEMBLY_SOLVE_FAILED: …An exception was thrown inside the solve (see below)Read the wrapped message; likely the solveMate escaped-filter invariant

The dof in the message is the sum over unsupported mates of UNSUPPORTED_DOF (coincident 3, concentric 4, distance 1, angle 1). tests/mateFns.test.ts has worked examples of every diagnostic, including the axis-plane and the a→b/b→a cycle.

Extending the solver (new mate or entity pair)

The load-bearing invariant: solveMate's default branch throws "solveMate: unsupported entity pair escaped filter" when solveTranslational returns null after isSupportedPair passed — i.e. the pair set and the dispatch switch drifted out of sync (surfaces as ASSEMBLY_SOLVE_FAILED).

To add a translational entity pair (e.g. a new axis-plane handling), update both in solverAdapter.ts:

  1. Add the key to TRANSLATIONAL_PAIRS.
  2. Add the matching case in the solveTranslational switch (with a per-pair solve function like the existing solvePlanePair, solveConcentric, solveAxisToPoint).

For a new orientation/axis mate, add to REQUIRED_ENTITIES and give it a branch in solveMate; add its type to POSITIONING_TYPES and UNSUPPORTED_DOF. Then surface it through mateFns.ts: extend the MateConstraint union and mateToSolverConstraint, and export from the public surface. See the adding-operations skill for the export/function-lookup.md gate and result-error-handling for adding an error code.

Show full SKILL.md (582 more words)Show less

Joints and kinematics

src/operations/jointFns.ts defines five joint types. A Joint connects parent (stationary reference) → child (moving); dofs is the source of truth; value/min/max mirror the primary (first) DOF for single-DOF ergonomics, and axis is the joint's anchor/primary axis (equal to the primary DOF axis only for single-axis joints, not for spherical/planar). Every DOF value is always clamped to [min, max] (makeDof normalizes inverted ranges).

JointDOFsDefault ranges
revolute1 rotation-180..180
prismatic1 translation (ignores axis.origin)0..100
cylindricalrotation + slide on one axisrot -180..180, trans 0..100
planaru-trans, v-trans, rotation about normalu/v -100..100, rot -180..180
sphericalx, y, z rotations about a pivot (Rx·Ry·Rz)each -180..180
  • Drive: setJointValues(joint, number[]) sets per-DOF positionally; setJointValue(joint, n) sets only the primary. jointTransform(joint, value?) returns the child's local pose — a single number overrides the primary DOF only, an array overrides positionally.
  • Forward kinematics: forwardKinematics(assembly, jointValues?) returns a world JointPose for every node, keyed by child name, resolved topologically: childWorld = parentWorld ∘ jointTransform ∘ offset?. Roots sit at origin. mechanismDOF sums open-chain DOFs (closed-loop Grübler/Kutzbach is future work).
  • Add a joint type: write a constructor building dofs via makeDof + buildJoint. FK/IK differentiate through dofs, so kinematics stays joint-agnostic automatically.

Driving: inverse kinematics and trajectories

src/operations/ikFns.ts. inverseKinematics(assembly, endEffector, target, options?) is damped-least-squares over a numerically-differentiated Jacobian of forwardKinematics — joint-type agnostic. IKTarget is { position, rotation? }; omit rotation for position-only (m=3 vs m=6). Options: maxIterations (200), tolerance (1e-5), damping (0.05), seed, tip. Gotcha: the finite-difference Jacobian steps inward from a bound, because forwardKinematics clamps each DOF — a forward +eps at a limit would give a zero column and trap the solver.

jointTrajectory(assembly, from, to, steps) samples a straight line in joint space, returning steps + 1 samples (both endpoints inclusive). Joints absent from from/to hold their stored value.

DH and URDF interchange

src/operations/dhFns.ts — jointsFromDH(rows, {base?}) builds a serial revolute/prismatic chain from a distal DH table. Each row → one joint; the variable is θ (revolute) or d (prismatic) about/along +z, and the fixed link geometry Rz(θ)·Tz(d)·Tx(a)·Rx(α) rides on Joint.offset so each row contributes exactly one DOF.

src/operations/urdfFns.ts — exportURDF round-trips only revolute/prismatic; it errors (VALIDATION_FAILED) on any multi-DOF joint or any joint carrying an offset (so DH chains cannot exportURDF). Revolute limits convert degrees↔radians on the boundary; prismatic emits a zero origin (brepjs prismatic FK ignores axis.origin). importURDF(xml) is a regex reader: continuous → revolute -180..180, and fixed/floating/planar joints are skipped (their links still listed).

Assembly STEP / XCAF export

exportAssemblySTEP(shapes: ShapeOptions[], { unit?, modelUnit? }) → Result<Blob> (MIME application/STEP). ShapeOptions is { shape, color?, alpha?, name? }. The OOP twin createAssembly returns a disposable AssemblyExporter (a KernelHandle); exportSTEP disposes it via Symbol.dispose. See src/operations/README.md for units/colors — do not restate them. This path needs the kernel's createXCAFDocument / writeXCAFToSTEP; assembly-STEP XCAF is a known occt-wasm divergence, so check the kernel-abstraction skill for current support. For handle disposal see memory-and-disposal.

Where things are exported (asymmetry to know)

Everything is in src/operations.ts and re-exported from root brepjs — except mates: addMate, solveAssembly, MateConstraint, MateEntity, AssemblySolveResult are exported only from root brepjs (src/index.ts), not from brepjs/operations. The solver core (solveConstraints, SolverEntity, SolverConstraint) is not barrelled at all — import it directly from @/kernel/solverAdapter.js, as the tests do.

Additional resources

  • tests/mateFns.test.ts — every solve + diagnostic case; tests/jointFns.test.ts, tests/dhFns.test.ts, tests/ikFns.test.ts, tests/urdfFns.test.ts, tests/assemblyFns.test.ts — usage examples.
  • src/operations/README.md — the STEP/XCAF export details (units, colors, disposal).
Sibling skills
  • result-error-handling — unwrapping Result, adding a BrepErrorCode.
  • kernel-abstraction — whether createXCAFDocument/writeXCAFToSTEP are supported per kernel.
  • adding-operations — the export surfaces + function-lookup.md gate when adding a mate/joint function.
  • writing-tests — the test skeleton.
  • memory-and-disposal — the AssemblyExporter KernelHandle and using.
  • debugging-geometry — for fused-compound "assemblies", which are a different thing entirely.

© andymai, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/assembly-solver of andymai/brepjs.

Open the folder on GitHubat commit ee50994

Compare with similar skills

Assembly Solver next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Assembly Solver compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Assembly Solver this skillandymai/brepjs114—~3.7kAutomated safety check: PassApache-2.0
Trellis Session Insightmindfold-ai/Trellis15k4 repos~1.7kAutomated safety check: PassAGPL-3.0
Native Data FetchingCherryHQ/cherry-studio-app4k6 repos~2.9kAutomated safety check: NotesMIT
Debugging Executionsn8n-io/n8n207k—~2.6kAutomated safety check: PassCustom licence
Aoti Debugpytorch/pytorch104k1 repos~1.7kAutomated safety check: PassCustom licence
Herdr Throwaway Reproductionherdrdev/herdr43k—~2.4kAutomated safety check: PassApache-2.0

Similar skills

  • Trellis Session Insight

    mindfold-ai/Trellis

    Reach into past AI conversation history through the trellis mem CLI.

    15k GitHub starsUsed in 4 repos~1.7k tokens
    DevelopmentAuto-check passed
  • Native Data Fetching

    CherryHQ/cherry-studio-app

    A skill your agent uses when implementing or debugging ANY network request, API call, or data fetching.

    4k GitHub starsUsed in 6 repos~2.9k tokens
    DevelopmentAuto-check: notes
  • Official

    Debug failed or wrong-output workflow executions using executions tools.

    207k GitHub stars~2.6k tokensUpdated today
    DevelopmentAuto-check passed
  • Aoti Debug

    pytorch/pytorch

    Debug AOTInductor (AOTI) errors and crashes. An agent skill from pytorch/pytorch.

    104k GitHub starsUsed in 1 repo~1.7k tokens
    DevelopmentAuto-check passed
  • Runs a disposable, uniquely named Herdr session inside an existing one so runtime, pane, terminal or API bugs can be reproduced without touching the main session.

    43k GitHub stars~2.4k tokensUpdated today
    DevelopmentAuto-check passed
  • Systematic Debugging

    ultralisp/ultralisp

    A skill your agent uses when encountering any bug, test failure, or unexpected behavior, before proposing fixes

    258 GitHub starsUsed in 51 repos~2.4k tokens
    DevelopmentAuto-check passed

More from andymai/brepjs

All 21 skills in this repo
  • Implement

    andymai/brepjs

    A skill your agent uses when authoring or editing a brepjs .brep.ts part — writing the geometry with the functional API (box, cylinder, fuse, cut, fillet, sketch→extrude…), declaring an expected…

    114 GitHub stars~3.4k tokensUpdated today
    Auto-check passed
  • Memory And Disposal

    andymai/brepjs

    This skill should be used when managing WASM handle lifetimes or hunting memory leaks in brepjs — when a task mentions "createHandle() without using keyword risks WASM memory leak"…

    114 GitHub stars~3.1k tokensUpdated today
    Auto-check passed
  • Polish

    andymai/brepjs

    A skill your agent uses when a valid brepjs part should look designed rather than glued-from-primitives (products, toys, mechanisms, anything a human eyeballs), and when exporting/handing off the…

    114 GitHub stars~588 tokensUpdated today
    Auto-check passed
  • Wasm Interop

    andymai/brepjs

    This skill should be used when working across the JS/WASM boundary in brepjs — writing or debugging code in src/kernel/occt, src/kernel/occtWasm, or src/kernel/brepkit, or diagnosing symptoms like…

    114 GitHub stars~3k tokensUpdated today
    Auto-check passed
  • Writing Tests

    andymai/brepjs

    This skill should be used when writing, running, or fixing tests in the brepjs repository — when a task says "add a test", "write a regression test", "tests are failing", "test timed out", "coverage…

    114 GitHub stars~4.3k tokensUpdated today
    Auto-check passed
  • Adding Operations

    andymai/brepjs

    This skill should be used when adding or extending a geometric shape operation in brepjs — the end-to-end recipe once the target module is chosen (which is decided by architecture-navigation) — when…

    114 GitHub stars~4.4k tokensUpdated today
    Auto-check passed

Categories

Questions about Assembly Solver

What does Assembly Solver do?

This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLYNOTCONVERGED / ASSEMBLYMATEINVALID / ASSEMBLYSOLVEFAILED…. Assembly Solver is an agent skill from andymai/brepjs. This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLYNOTCONVERGED / ASSEMBLYMATEINVALID / ASSEMBLYSOLVEFAILED, adding or extending a mate or solver entity-pair, adding a joint type, or driving a mechanism.

When should I use Assembly Solver?

Assembly Solver fits situations like: phrases include assembly wont converge; unsupported constraint types; solveAssembly returns Err; add a mate/joint/constraint type.

How do I install Assembly Solver in Claude Code?

Run `npx skills add andymai/brepjs --skill assembly-solver -a claude-code`. Or copy the skill folder (.claude/skills/assembly-solver in andymai/brepjs) into .claude/skills/assembly-solver in your project. Claude Code loads it when a task matches its description.

How do I install Assembly Solver in Codex?

Run `npx skills add andymai/brepjs --skill assembly-solver -a codex`. Or copy the skill folder (.claude/skills/assembly-solver in andymai/brepjs) into .agents/skills/assembly-solver in your project. Codex loads it when a task matches its description.

Can I use Assembly Solver in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add andymai/brepjs --skill assembly-solver -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/assembly-solver, .gemini/skills/assembly-solver, .github/skills/assembly-solver and .opencode/skills/assembly-solver in your project.

What does Assembly Solver need to run?

SKILL.md names no scripts, command-line tools or credentials: Assembly Solver is instructions for the agent only.

Does Assembly Solver access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Assembly Solver safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Assembly Solver use?

Assembly Solver is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Assembly Solver use?

About 3.7k tokens (SKILL.md is roughly 15k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Assembly Solver?

Skills that share tags, products or a category with Assembly Solver: Trellis Session Insight (mindfold-ai/Trellis, 15k stars), Native Data Fetching (CherryHQ/cherry-studio-app, 4k stars), Debugging Executions (n8n-io/n8n, 207k stars) and Aoti Debug (pytorch/pytorch, 104k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Assembly Solver?

andymai (a GitHub user) maintains it in andymai/brepjs, which has 114 GitHub stars. The repository holds 21 skills in this directory. The repository was last updated on October 8, 2026.

Source: andymai/brepjs on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.