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

@coursefoundry/cli

v0.9.0

Published

Local CLI for CourseFoundry - manage and sync your courses from the terminal.

Readme

@coursefoundry/cli - the cf command

A local CLI for CourseFoundry. Manage and (soon) sync your courses from the terminal, authenticated with a cfry_ API key - no GitHub required.

Status: Phases 1–5 - auth, read-only commands, full local folder sync (clone / status / pull / push), local Quarto preview, and authoring commands (publish, new lesson, workflow run, watch).

Install

Requires Node 20+. The package is published to npm as @coursefoundry/cli:

npm i -g @coursefoundry/cli   # installs the `cf` command
cf --version

Or run it without installing:

npx @coursefoundry/cli whoami

Upgrade with the same npm i -g command. When a cf login or cf whoami finds that your copy is older than the oldest version CourseFoundry still supports, it prints one line naming the install command; nothing is refused.

Development

From a checkout of this repository, in cli/:

npm install
npm run build
npm link        # makes your local build the `cf` on your PATH

During development you can run without building:

npm run dev -- whoami

npm test runs the CLI's own vitest suite, and npm pack --dry-run shows exactly what a release would ship (dist/, README.md and package.json, nothing else). A release is cut by tagging cli-v<version> on main; see docs/deploy.md.

Authenticate

cf login                      # browser sign-in (device flow) - opens CourseFoundry to approve
cf login --no-browser         # prints the URL + code instead of opening a browser
cf login --scope read         # request a read-only key
cf login --force              # re-authenticate even if already logged in
cf login --key cfry_xxx       # skip the browser; store a key you already have
CF_API_KEY=cfry_xxx cf whoami # one-off, no stored login

Browser sign-in (cf login with no --key): the CLI shows a short code and opens CourseFoundry's /cli-auth page. You approve there, and a cfry_ key is minted and returned to the CLI automatically - no copy-pasting. If the server doesn't support the device flow, cf login falls back to pasting a key (create one under Settings → MCP API Keys).

Key hygiene - three things keep keys from piling up:

  • cf login is idempotent: if a valid login is already stored it does nothing (use --force to deliberately re-authenticate).
  • Keys are named per device (cf CLI · <hostname>); signing in again on the same machine revokes the previous key for that device, so you keep one active key per machine rather than one per login.
  • cf logout revokes the key on the server (not just locally). Use cf logout --local to keep the server key and only forget it on this machine.

The key is stored at ~/.coursefoundry/config.json (chmod 600).

Commands

Account

| Command | Description | |---|---| | cf login | Save and verify a cfry_ API key | | cf logout | Remove the stored key | | cf whoami | Show the authenticated account (plan, level, XP, credits) | | cf courses list [--status draft\|published\|all] | List your courses |

Sync (run inside a cloned course folder)

| Command | Description | |---|---| | cf clone <id\|slug> [dir] [--force] | Download a course into a local folder + write the sync manifest | | cf status [--offline] | Show what changed locally vs on the server. --offline compares with the last sync only (no login, no network) and lists what cf push will send; plain cf status falls back to that view, after one warning line, when the server is unreachable | | cf pull [--force] | Bring server changes down; --force overwrites locally-conflicting files | | cf push [--ours\|--theirs\|--force] | Send local changes up; refuses on conflict unless a resolution flag is given | | cf check [--ops <file>] | Validate the clone offline - no login, no network: what cf push would refuse (invalid paths, images over 3 MB, oversize text, files the sync ignores), what a render or an AI edit would trip on (missing frontmatter, _quarto.yml links to missing files, image references that resolve to no file, a locally edited specification.md), and with --ops a dry run of a harness-op batch. See below | | cf render [--native] [--no-standalone] [--offline] [--open] [--tier t] [--engine e] [--only p...] | Render the clone locally in the pinned CourseFoundry container (or with your Quarto); cf render doctor / pull / clean beside it | | cf preview [--no-quarto] [--no-extensions] | Live local preview via a local Quarto install (graceful fallback if absent). First installs any catalogue extension the course's decks/pages reference but _extensions/ lacks (codefrag, tabset, fragmention, cascade, webr, pyodide), at the same pins the CI render uses |

Image size limit: each image (.jpg/.jpeg/.png/.webp/.gif/.svg) may be at most 3 MB. Before uploading anything, cf push checks every added or modified image (and, with --ours/--force, every conflicting one it would send); if any is over the limit it uploads nothing and fails with image_too_large, listing each oversize image with its size. Compress or resize them and push again. Other files keep their existing limits.

Never synced, in either direction: Quarto build output (_output/), the generated CI workflows (.github/workflows/publish.yml and render.yml), catalogue extensions installed as _extensions/<owner>/<name>/, and coding-agent guidance (AGENTS.md, AGENTS.local.md, CLAUDE.md, CLAUDE.local.md at any depth, and anything under .claude/). cf status lists any such files still stored on the server so they can be deleted in the web editor; your own extensions and workflows keep syncing.

Import (run inside a cloned course folder)

| Command | Description | |---|---| | cf import <path...> | Convert local files into course content. Accepts files, directories (recursive), and glob patterns | | --into <dir> | Destination directory inside the course (default: course root) | | --dry-run | Show the plan and exit without writing | | --overwrite | Replace files that already exist (default: skip them) | | -y, --yes | Apply without asking for confirmation | | --ai | Send extracted text to the AI import workflow for structuring (uses AI credits) | | --push | Push the imported files after a successful import | | --title <title> | Lesson title; single file only |

Authoring

| Command | Description | |---|---| | cf publish [--off] | Publish, or unpublish with --off | | cf new lesson <path.qmd> [--title] | Create a lesson on the server and sync it locally | | cf workflow run <type> [...] | Run an AI workflow (outline, lesson, slides, assessment, review, import, suggest_outcomes) | | cf prompt [message] [-y] | AI chat about the current course (uses AI credits). See below | | cf watch [--ours\|--theirs\|--force] | Auto-push local changes as you edit (debounced) | | cf agents init [--claude] [--codex] [--force] | Write coding-agent guidance into the clone (both agents by default; existing files are skipped unless --force). Local only - never synced or published. See below | | cf placeholders list [--unresolved] | List the course's image placeholders: id prefix, label, type, size, status, reference count, suggested image path and prompt. --json gives full ids and every reference | | cf placeholders apply <id> <image> / --map <file> [-y] | Upload local image(s), replace the placeholder(s) everywhere they are used, then pull the rewritten files. See below |

Prompt mode (cf prompt)

Run inside a cloned course folder. With a message it's a one-shot question; without one it opens an interactive chat session (/help, /clear, /exit):

cf prompt "what does week 3 cover?"
cf prompt "add a lesson on error handling to week 4"
cf prompt            # interactive

It's the same assistant as the web editor's Chat panel: the AI sees the course structure, can read course files, and proposes file changes as an ops batch instead of applying them. The CLI shows the batch, asks for confirmation (-y/--yes skips the prompt), applies it server-side, and then pulls so the local clone matches. Files that also changed locally are left untouched and reported as conflicts - resolve them with cf pull --force or cf push.

If any of the course's enabled skills shaped the answer, the reply is followed by a line naming them and where each came from:

Skills used: deck-doctor (built-in), house-style (yours)

Nothing is printed on a turn where no skill was loaded. --json carries the same information as skillsUsed (null when none).

The AI can also propose changes to a single slide of a RevealJS deck rather than rewriting the whole file. Those lines name the deck and the slide position - counted from 1, in the deck's numbering before any of the batch is applied:

  ~ edit    slides/week01.qmd  slide 4
  + insert  slides/week01.qmd  new slide at position 4, after slide 3
  - delete  slides/week01.qmd  slide 3, removed from the deck
  → reorder slides/week01.qmd  slide 1 moves to position 3 once it has been lifted out

A reorder's destination counts the deck with the moved slide already lifted out, so [A,B,C] moved 0 -> 2 gives [B,C,A].

--json works in one-shot mode only and never prompts: without --yes the proposed batch is returned in data.harnessOps but not applied.

Using an AI coding agent (cf agents init)

Run cf agents init inside a cloned course to tell a coding agent how the course works:

  • Claude Code (--claude): .claude/CLAUDE.md plus a coursefoundry-placeholders skill at .claude/skills/coursefoundry-placeholders/SKILL.md, which Claude Code loads when you ask it to fill image placeholders or add course images.
  • Codex (--codex): a root AGENTS.md.

With neither flag it writes both. The guidance covers the course path rules (letters, digits, - _ . /, no spaces), images as course files under images/, the 3 MB image limit, and the placeholder workflow: cf placeholders list --json, produce the images under images/, then cf placeholders apply --map <file> --yes. It also tells the agent never to edit placeholder URLs by hand and not to run cf watch during a run.

A file that already exists is left unchanged and reported; --force overwrites it. --json reports { written, skipped, warnings }. The guidance files never leave your machine: cf push/pull skip them in both directions, cf status lists any copy already stored on the server, and cf clone never writes them. Quarto 1.9 and later does not render AGENTS.md/CLAUDE.md as pages; on an older Quarto, cf agents init and cf preview warn that a root AGENTS.md/AGENTS.local.md/CLAUDE.md/CLAUDE.local.md will show as a local preview page (it is still never published) and suggest upgrading. The quarto preview command itself is unchanged.

Filling image placeholders (cf placeholders)

cf placeholders list shows every image placeholder in the course; --unresolved hides the ones already replaced, and --json returns the full records (ids, prompts, briefs, pixel sizes, every referencing file and line, and a suggested path under images/).

cf placeholders apply replaces placeholders with images you have put in the clone, in one step:

cf placeholders list --json > /tmp/placeholders.json   # an agent reads this
# ... produce images/pump-diagram.svg and images/valve-photo.png ...
cat > /tmp/fill.json <<'JSON'
{ "replacements": [
  { "placeholder": "3f2a9c1e", "image": "images/pump-diagram.svg" },
  { "placeholder": "8b0d4e77-5c1a-4f0e-9a2b-1c3d5e7f9a0b", "image": "images/valve-photo.png" }
] }
JSON
cf placeholders apply --map /tmp/fill.json --yes
cf placeholders apply 3f2a9c1e images/pump-diagram.svg     # a single pair, asks first
  • Ids are the full id or a unique prefix of at least 8 characters; an unknown or ambiguous one stops with usage before anything is uploaded.
  • Image paths are course paths, relative to the clone root (the same form as suggested_path). A path relative to the current directory, or an absolute one, is also accepted when it lands inside the clone. The map file can live anywhere and is read relative to the current directory - keep it outside the clone, since a .json inside would be pushed as a course file.
  • Every image is checked first - inside the clone, course path rules, a supported type (.png .jpg .jpeg .webp .gif .svg), at most 3 MB, and a regular file cf actually syncs (not a symbolic link, and not inside a folder cf skips such as _freeze/, _site/ or .quarto/). Any failure stops the run with image_invalid, naming each image and the rule, and nothing is uploaded.
  • Confirmation: the plan is shown and you are asked before anything changes. Without a terminal, or with --json, --yes is required; otherwise it stops with confirmation_required before any server call.
  • Upload goes through the normal sync engine, but only for the chosen images - other local edits stay unpushed. An image that also changed on the server stops the run with push_conflict before anything is replaced.
  • Replacement rewrites every reference to each placeholder (lessons, slides, _quarto.yml, notebooks) to a path relative to the file, all pairs together or none. If CourseFoundry refuses, every failing pair's reason is printed (placeholder_refused); the images stay uploaded, so a corrected retry does not send them again. Under --json the error envelope also carries reason and failures ([{ placeholder_id, image_path, code, message }]) beside code and message.
  • Pull: the rewritten files are then pulled. A file you also edited locally is left untouched and reported as a conflict, with the commands that resolve it. --json returns { uploaded, resolve, pulled }.

Checking a clone offline (cf check)

cf check reads the clone and reports, without logging in or touching the network:

| Severity | Code | Meaning | |---|---|---| | error | path_invalid | A path the server refuses: anything outside letters, digits, - _ . / (so no spaces), a leading slash, ./.. segments, over 500 characters | | error | image_too_large | An image over the 3 MB limit (cf push refuses it too) | | error | content_too_large | A text file over the server's 15,000,000-character limit | | info | not_synced | An extension the sync never uploads (.txt, .pdf, no extension…) - cf push ignores it | | warning | frontmatter | A .qmd/.md without a YAML block, with an unclosed one, or without title: - the same sentence the AI harness gives | | warning | sidebar_href_missing | An href: in _quarto.yml that names no file in the clone - the render drops it | | warning | image_missing | A markdown image (![alt](path), with or without {...}) or raw <img src> in a .qmd/.md that names no file in the clone, resolved relative to the referencing file (or to the course root when it starts with /), case-exact since CI renders on Linux. The website render shows a broken image; the PDF render fails on it. URLs, data: URIs and anchors are ignored, as are fenced code blocks and platform placeholder addresses | | warning | managed_modified | The root specification.md differs from the last sync: Programme Designer regenerates it and local edits are overwritten |

Managed paths (_output/, the generated workflows, catalogue extensions, agent guidance) are skipped exactly as the sync skips them. Exit status is 1 only when an error finding exists - what the server would refuse. Warnings and infos never fail the run.

cf check --ops <file> additionally dry-runs a harness-op batch against the clone with the file-op rules the server applies (batch_limit, protected_path, invalid_path, collision, unexpanded_intent, plus frontmatter warnings). The file may be a bare { "ops": [...] }, the { "harnessOps": ... } payload, or a whole cf prompt --json envelope (it reads data.harnessOps). Slide, plan, hidden, theme, sidebar and git ops are reported as not_checked - the server validates those on apply. Keep the file outside the clone, since a .json inside would be pushed as a course file.

cf check
cf prompt "tidy week 3" --json > /tmp/proposal.json && cf check --ops /tmp/proposal.json
cf check --json   # ok:true with data.findings, or ok:false code check_failed with the same findings in error

A clean cf check is not a promise that a push succeeds: storage quota, entitlement and conflicts are decided online by the server, and _brand.yml values are validated there only. Every rule here is a mirror of the server's or core's, pinned by drift tests in cli/.

Working offline

Nothing in the CLI needs a connection except talking to the server: cf check and cf status --offline run entirely locally, and editing a clone needs nothing at all. When a command that does need the server cannot reach it, it says so in one line (naming the server, noting that local edits are safe, and pointing at cf status --offline) and exits with the offline error code under --json - never a stack trace, and never a hang: every request has a timeout (5 minutes, since AI turns can be long).

  • cf status --offline compares the clone with the manifest written at the last sync, so it lists exactly the adds, edits and deletes cf push will send when you reconnect. It says which sync time it compared against; server-side changes are unknown until then.
  • cf pull / cf push offline stop with that one line.
  • cf prompt probes the server (5 s) before any turn and refuses offline: the AI runs only on the server, never on your machine.
  • Reconnecting: cf push runs the usual three-way diff, so a day of offline edits goes up cleanly and only a file changed on both sides since the last sync is reported as a conflict (--ours / --theirs resolve it per file).

Rendering locally (cf render)

cf render                  # render the clone in the pinned CourseFoundry container
cf render --native         # no container: Quarto on your PATH (NOT parity - it says so)
cf render --no-standalone  # skip the per-resource HTML+PDF pass (much faster)
cf render --offline        # use the last prepared tree; no network
cf render --open           # open the rendered site when done
cf render doctor           # engine, image, disk, cached tree, Quarto on PATH
cf render pull             # pre-pull the image (0.5-1 GB the first time)
cf render clean            # delete the output, the cached tree and the cache volume

What it renders is what the cloud renders. The clone's files are sent to POST /api/courses/<id>/render-tree, where CourseFoundry applies the same push-time transforms the commit route applies (brand merge, deck options, the _quarto.yml patch, the hidden-file manifests) and answers only what changed. That overlay goes onto a staged copy of the clone, which is rendered in the cf-render container image pinned by digest - the exact Quarto the cloud uses. Your files are never written: the output lands in .coursefoundry/render/out/ (rendered/, standalone/, render-report.json), the cached tree in .coursefoundry/render/tree.json, and the stage under your OS temp directory (Quarto renders nothing from inside a hidden folder, so the stage cannot live in .coursefoundry/). Nothing under .coursefoundry/ is ever synced.

Needs Docker Desktop (or Podman) running; cf render doctor tells a stopped engine from a missing one. Without an engine, --native renders with the Quarto on your machine and prints a not-parity banner (and a version warning against the pin); the standalone pass runs only in the container. --offline renders from the last prepared tree and lists every file that changed since - those render unpatched. --only <files> is --native only. Exit status is 0 iff the site rendered; the standalone pass is best-effort, exactly as in CI. --json emits the report (render_failed, no_engine, no_image, no_render_cache as error codes).

A long run is visibly alive (0.9.0+). The container streams Quarto's [n/N] path lines as the site renders and one [i/N] path html ok 4.1s pdf ok 12.3s line per resource through the standalone pass; whenever nothing has printed for 5 s the CLI shows rendering… 40 s on stderr - redrawn in place on a terminal, one line per interval when piped - and clears it when the run ends. All of it is stderr: under --json stdout is still exactly one envelope. The per-file table at the end is unchanged. (The streaming needs the render-image-v2 image or later; against an older pin only the heartbeat shows.)

Machine-readable output

Add --json to status, pull, push, import, check, agents init, or placeholders list/apply. Every response is a single document on stdout wrapped in a versioned envelope, successes and failures alike:

{ "schema": "cf-cli/1", "command": "import", "ok": true, "data": { } }
{ "schema": "cf-cli/1", "command": "import", "ok": false,
  "error": { "code": "confirmation_required", "message": "…" } }

Some errors add structured fields beside code and message (currently placeholder_refused: reason, failures; check_failed: findings, counts, checkedFiles); ignore keys you do not know.

Error codes: not_in_clone, no_sources, confirmation_required, not_authenticated, write_failed, extractor_unavailable, push_conflict, image_too_large, image_invalid, placeholder_refused, check_failed, offline, usage, internal.

Under --json the CLI never prompts, so import and placeholders apply additionally require --yes to apply. whoami and courses list still emit their own bare --json shapes.

Breaking change in 0.5.0: cf status --json previously emitted its payload at the top level. It is now wrapped in the envelope above - read data.changes rather than changes.

Importing local files

cf import converts material on your machine into course files. Everything happens locally: nothing is uploaded unless you pass --ai or --push.

With --ai, the number of files to be sent is reported before anything is spent. If structuring fails for a file, the import keeps that file's raw extraction and says so (! file.md: AI enrichment failed …; kept the raw extraction.) - a fallback is never silent, so a success line always means what it says.

| Source | Produces | |---|---| | PDF | Lesson text, embedded images, title from document metadata | | Word (.docx) | Markdown with heading levels, list nesting, emphasis and embedded images | | PowerPoint (.pptx) | One ## heading per slide, speaker notes as a ::: {.notes} block, embedded media | | Markdown / text | Passed through; existing YAML front matter is merged, not nested | | Images | Placed under images/; no lesson generated |

Nothing is written until you see the plan and confirm. Files that already exist are skipped unless you pass --overwrite. Imported files land in your working copy as ordinary local changes, so they reach the server through the normal cf push three-way diff - import never writes to the server itself and never touches the sync manifest.

cf import ~/lectures/week3.pptx --into week3 --dry-run
cf import ~/lectures --into week3 --yes
cf import handout.pdf --ai --push

If a local Pandoc is on your PATH it is used for Word documents, which handles tables and complex lists better. Its absence changes nothing.

Scanned, image-only PDFs report as empty rather than producing a blank lesson - OCR is not included. Legacy .doc/.ppt binaries are detected and refused with a note to re-save them in the modern format.

How sync works

cf clone writes the course files plus a .coursefoundry/manifest.json recording a sha256 per file. cf status / pull / push do a three-way diff - base (manifest) vs local (disk) vs remote (list_files) - so they can tell a local edit from a server edit and flag files changed on both sides as conflicts. Hashes are computed identically on client and server, so only real changes move. Binary files (images) are handled as base64; build artefacts (_site/, .quarto/, _freeze/) and non-course files are ignored.

Workflow examples

# Generate a course outline to stdout
cf workflow run outline --title "Intro to Rust" --description "Ownership, traits, async"

# Draft a lesson body into a file, using the current clone's course context
cf workflow run lesson --course-title "Intro to Rust" --lesson-title "Ownership" --out ownership.qmd

# Anything not covered by a flag: pass raw JSON context
cf workflow run assessment --content lesson.qmd --ctx '{"courseTitle":"Intro to Rust"}'

Global options & environment

| Flag | Env | Default | |---|---|---| | --api-base <url> | CF_API_BASE | https://www.coursefoundry.com | | CF_RENDER_IMAGE | cf render only: a local image reference to run instead of the pinned digest (for testing an unpublished cf-render build; prints a not-parity line) | | --key <token> | CF_API_KEY | (stored login) |

--api-base is handy for pointing at a local dev server, e.g. cf --api-base http://localhost:5173 whoami. The bare coursefoundry.com apex is auto-upgraded to www. to avoid a redirect that drops POST bodies.

Inside a cloned course, the server is chosen in this order: --api-base, then $CF_API_BASE, then the address saved when the course was cloned, then your stored login. So CF_API_BASE=https://<preview>.vercel.app cf placeholders list talks to that preview even in a clone made against production.

How it works

The CLI is a thin JSON-RPC 2.0 client over CourseFoundry's MCP endpoint (POST /api/mcp). courses list calls the list_courses tool; whoami reads the coursefoundry://user/profile resource. The create_course tool accepts an optional workspace (personal or organisation) naming which of your two workspaces the course belongs to - your own plan or the joined organisation's plan then governs its course count, storage and AI credits; omitted, it lands in your active workspace. See docs/mcp-integration-guide.md.