npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@peterseibel/bhs-content

v0.8.2

Published

Shared client library and CLI for the bhs-cs content, question-bank, and grading APIs.

Downloads

3,069

Readme

@peterseibel/bhs-content

Shared client library and CLI for the bhs-cs server APIs: content-file sync, course calendars and lesson plans, the question bank and quizzes, student-work reads for grading, and grades write-back. Used by bhs-cs's push scripts, the lesson-planning app, and the grading scripts.

This is a repo-internal workspace package, not published to npm: it lives and versions with the server so client and API move together, and the apps link it through the npm workspace symlinks (buildless TypeScript — its exports point at src/*.ts).

Requires Node >= 20 (uses global fetch).

Library

import { BhsContentClient, contentSha } from '@peterseibel/bhs-content';

const client = new BhsContentClient({
  server: 'https://bhs-cs.example.com',
  apiKey: process.env.BHS_CS_API_KEY,
});

Every method sends Authorization: Bearer <apiKey>. Non-2xx responses throw an Error carrying status (the HTTP status) and body (the response body text). Generic verbs get(url), post(url, body?, contentType?), put(url, body?, contentType?), and delete(url) are available alongside the typed methods; bodies are JSON-encoded unless another content type is given.

Content files

The server stores pushable content under a content root mirroring the serving roots views/, public/, static/, and standards/. Paths are always relative to that root.

  • contentManifest(prefix?){ files: { path: { sha256, size, mtime } } } (mtime is epoch ms).

  • getContentFile(path)Buffer of the raw bytes.

  • putContentFile(path, bytes){ ok, path, sha256, size }.

  • deleteContentFile(path).

  • batchContent(tarballBuffer) — POST a gzipped tar of files; the tar may include a special .bhs-content/delete.json entry (a JSON array of paths) which the server processes as deletions. Returns { ok, written: [...], deleted: [...] }.

  • diffContent(localDir, { prefix? }){ added, changed, removed, unchanged } (arrays of paths) vs the server manifest.

  • syncContent(localDir, { prefix?, delete?, dryRun? }) — the manifest-diff push. Walks localDir (whose layout mirrors the roots, e.g. localDir/views/..., localDir/static/...), sha256s each file, diffs against the server manifest, and pushes all additions and changes as a single batch tarball. With delete: true it also removes server files missing locally (limited to prefix when given). Returns { pushed: [...], deleted: [...], unchanged: n }; with dryRun: true it returns the plan without pushing.

buildContentTarball(localDir, files, deletions?) (exported) builds the batch tarball by hand if you need it.

Calendar and lesson plans

  • putCalendar(courseId, calendar) / getCalendar(courseId) — calendar is { units: [ { title, weeks?, children: [ { title, type?, days?, children: [ { title } ] } ] } ] }.

  • lessonPlanManifest(courseId){ plans: { lessonName: contentSha } }.

  • createLessonPlan(data), getLessonPlans(courseId), getLessonPlan(courseId, lessonName), deleteLessonPlan(courseId, lessonName).

Question bank and quizzes

  • bankManifest(){ questions: { uuid: contentSha } }.

  • putQuestion(uuid, question) — question is { label?, bodyHtml, answerSpec, sourcePath?, contentSha, tags: [...] }; answerSpec is { choices: [{html,text,correct}...], explanation?, allowedForms?, ordered? } — one list in authored/display order, correctness a per-choice flag.

  • getQuestion(uuid), deleteQuestion(uuid), questionsByTag(tags) (tag intersection).

  • putQuiz(courseId, kind, name, quiz, { force? }) — quiz is { title, questions: [ { uuid, form, options? } ] } (array order is display order); force permits removing or form-changing questions that already have answers. getQuiz(...), deleteQuiz(..., { force? }).

  • syncQuestions(questions) — array of { uuid, ...question }; diffs against the bank manifest by contentSha and pushes only new or changed questions. Returns { pushed, unchanged }.

Work reads (grading)

classRoster(classId), courseRoster(courseId), journalEntries(courseId, { promptId?, assignmentId? }), quizGrid(courseId, kind, name, { classId?, saves? }), codingConfig(url), assignments(), assignment(id), reflectionGradeData(id?), completedSpeedruns(), startedSpeedruns(), studentGithubHandles(), grades().

Grades write-back

  • postGrades(grades) — array of { userId, assignmentId, standard, score, grade }.

  • replaceAssignmentGrades(assignmentId, { grades, excused? }) — scoped replace for one assignment; excused is [{ userId, reason }].

  • clearAssignmentGrades(assignmentId), postGradedSpeedruns(list), gradeJournalEntry(entryId, { ok, words?, lateExcuse? }).

Course administration

  • putCourse(courseId, { title, active? }).

Content shas

contentSha(objOrBuffer) implements the convention used everywhere: sha256 hex of the raw bytes for Buffers and strings (content files), or of the canonical JSON serialization — JSON.stringify with keys sorted recursively (canonicalJSON) — for objects (questions, lesson plans). objectContentSha(obj) is the same minus any uuid/contentSha bookkeeping fields, which is how the CLI and syncQuestions compute shas for payloads that carry their own sha.

CLI

npx bhs-content [--server <url>] [--key <apiKey>] [--target <name>] <command>

(Registry installs get the bhs-content and push-content bins from the compiled dist/. Inside the monorepo workspace — which is buildless, no dist/ — run the sources directly instead: node bhs-content/src/bin/bhs-content.ts / node bhs-content/src/bin/push-content.mjs.)

push-content is the separate zero-dep overlay mirror used by the content-publisher repos (bhs-cs-content, bhs-cs-bhsawesome); see the header of src/bin/push-content.mjs for its usage.

Server and key resolve in order: explicit --server/--key flags, the named --target entry in ./.bhs-content.json, then the BHS_CS_SERVER / BHS_CS_API_KEY environment variables.

.bhs-content.json (in the directory you run the CLI from) maps target names to server configs:

{
  "local": { "server": "http://localhost:3000", "apiKey": "dev-key" },
  "prod": { "server": "https://bhs-cs.example.com", "apiKey": "..." }
}

Don't commit real keys; keep the file out of version control or keep prod keys in the environment instead.

Commands

# Content sync (dir mirrors views/, public/, static/, standards/)
bhs-content push <dir> [--prefix <p>] [--delete] [--dry-run]
bhs-content status <dir> [--prefix <p>]    # summary counts
bhs-content diff <dir> [--prefix <p>]      # per-file A/M/D lines

# Lesson plans
bhs-content lesson-plans manifest <course>
bhs-content lesson-plans push <file.json>  # computes contentSha from canonical JSON
bhs-content lesson-plans delete <course> <lesson>

# Question bank (file basename or uuid field is the uuid)
bhs-content questions push <file-or-dir>

# Quizzes
bhs-content quiz push <course> <kind> <name> <file.json> [--force]
bhs-content quiz get <course> <kind> <name>

# Grades
bhs-content grades push <file.json>                      # array → PUT /api/grades
bhs-content grades replace <assignmentId> <file.json>    # scoped replace

# Calendar
bhs-content calendar push <course> <file.json>

Development

npm install
npm test        # node test/smoke.js — no network needed