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

@sdelsad/commodity-desk-toolkit

v1.16.0

Published

One-command publishing for the Soft Commodity Trading podcast: the daily session writes two files and runs run_episode.py, which builds the page, charts, e-mail and feed and stages them on npm for the connector to copy server-side

Readme

Soft Commodity Trading — toolkit

Reusable, tested scripts behind the Soft Commodity Trading podcast: a daily 10-minute briefing on physical commodity trading. Published so the daily automation runs the same code every morning instead of re-deriving it.

Naming — read this before renaming anything

The show is called Soft Commodity Trading. It was previously Commodity Desk Daily; that name must not appear in any script, page, email or feed any more.

The slug is frozen at commodity-desk-daily on purpose. It is the npm package name, the GCS folder and every published episode URL. Renaming it would break the RSS enclosure URLs of episodes already downloaded and force every subscriber to re-subscribe. Display names change; the slug does not.

Install (once per fresh container)

npm pack @sdelsad/commodity-desk-toolkit && tar xzf sdelsad-commodity-desk-toolkit-*.tgz
bash package/setup.sh          # installs deps, downloads the Kokoro voice model (~350 MB, cached)

The package also carries the show's knowledge, so the daily run reads it instead of re-deriving it:

  • STYLE.md — the show bible: audience, framing rules, how it sounds, episode structure, quiz standards. Read it before writing.
  • curriculum.md — 30 dense episode briefs (6 weeks): angle, worked example, and the depth to reach. Two concepts per episode, or one taken deep.
  • conversions.md — 12 unit-conversion drills, one per episode on a rotating cycle, email only: the rule, the fast mental method, worked examples.

What builds what

| Script | In | Out | |---|---|---| | fetch_context.py | npm history | covered.md, prev_epNN.md | | generate_audio.py | script.txt | epNN.mp3 | | publish_episode.py | epNN.md + mp3 | npm package, feed.xml, covered.md, glossary.md | | build_page.py | epNN.md | epNN.html, epNN_chartN.png | | build_email.py | epNN.md | epNN_email.html, epNN_email.txt | | build_post.py | an essay .md | writing/<slug>.html, index.json, index.html | | chart.py | a ```chart block | inline SVG (page), PNG (e-mail) |

Write nothing by hand that one of these renders. The page and the e-mail are built from the same epNN.md, which is why they agree; anything typed straight into HTML drifts within a week.

0. Fetch what has already aired

python3 package/fetch_context.py --episode 12 --outdir .

Writes covered.md (running log of every episode aired) and prev_epNN.md (the J-1 and J-3 episode notes, for the quiz) into the working directory, by walking back through published versions. Always exits 0 — missing history is normal early on and must never block an episode.

1. Generate the audio

The spoken script is one segment per line, with the pause (in seconds) that should follow it. This is what makes the narration breathe instead of sounding like a machine reading an article.

Here is the thing nobody tells you about commodity trading. ||| 0.5
A merchant does not get paid for predicting prices. ||| 0.35
Space. Time. Form. ||| 0.7

Conventions: 0.3 mid-thought, 0.5 after a completed idea, 0.7–0.8 after a punchline or section break. Lines starting with # are ignored.

Dialogue lines use an uppercase speaker label and are rendered with two distinct voices when generated through the podcast-tts connector:

BUYER: November Santos, what have you got? ||| 0.25
SELLER: I make you plus eighty-five. ||| 0.25
python3 package/generate_audio.py script.txt ep02.mp3 --voice am_michael --speed 0.97

Prints DURATION_SECONDS= and SIZE_BYTES= for the publish step. A segment that fails synthesis is skipped rather than killing the episode.

2. Publish it

Publishes to npm, which jsDelivr serves as a public CDN. Reads the currently live version, bumps the patch, carries the cover art, RSS feed, covered.md log and glossary.md forward, and prepends the new episode to the feed (re-publishing the same episode number replaces its item rather than duplicating it).

Two modes: --mp3 <file> embeds a local mp3 in the npm package (Kokoro fallback path); --mp3-url <url> --size-bytes <n> references an externally hosted mp3 (Gemini/GCS path — the feed and email point straight at it).

Authentication: export NPM_TOKEN and the script writes a token-free .npmrc next to the staged package, which npm expands at publish time. Never write the token itself into a file or a config command.

export NPM_TOKEN=npm_...
python3 package/publish_episode.py \
  --mp3 ep02.mp3 --notes ep02.md \
  --number 2 --title "Flat Price vs Basis" \
  --description "Why physical traders don't bet on price." \
  --covered "Basis vs flat price; long/short the basis; basis P&L. Vocab: differential, basis risk. Example: Santos cargo." \
  --glossary "lot = one futures contract, the unit desks count positions in" \
  --covered-out covered.md --glossary-out glossary.md \
  --duration 612 --pubdate "Tue, 11 Aug 2026 05:00:00 GMT"

Add --dry-run to stage the package without publishing. Prints PUBLISHED_URL=, PUBLISHED_VERSION= and FEED_URL=.

Linking the feed to the web

--page-url gives the episode a website. Apple Podcasts turns an item's <link> into the episode's website button and renders anchors inside a CDATA <description>, so the publisher writes both, plus an <itunes:summary> with the bare URL for apps that strip HTML. The channel <link> points at the show's site (--site-url), which is what Apple shows as the podcast's website.

The page URL is deterministic, so it can be passed in the same command that publishes the audio, before the page itself exists:

--page-url "https://storage.googleapis.com/podcast-audio-2647223968/commodity-desk-daily/epNN.html"

3. Build the episode page

python3 package/build_page.py --notes ep03.md --number 3 \
  --title "Futures Plumbing and the Shape of the Curve" \
  --dek "One sentence under the headline." \
  --audio-url "https://.../ep03.mp3" --duration 620 \
  --date "Wednesday 12 August 2026" --out ep03.html

Renders the notes into a standalone page and hosts with the connector's publish_page tool. What the page carries:

  • A contents rail built from the H2s and H3s, fixed in the left margin above 1240px and inline below it, with the section being read highlighted. Every heading gets a stable id and a hover anchor.
  • Previous / next episode links and an archive of every episode aired.
  • The glossary as it stood that day, collapsed. Episode 1 carries episode 1's terms, episode 5 carries one to five — a page dated Monday listing a word first used on Friday reads as a leak. A search box and one chip per episode narrow it further; the chips are omitted when there is only one episode to show.
  • One reveal per quiz answer — checking Q1 no longer burns Q2 to Q7 — plus reveal-all / hide-all, and a link back to each question.
  • Tables that survive a phone: each one scrolls inside its own box, figures are tabular and right-aligned, and moves in a change column carry an explicit sign and a restrained green or red.
  • schema.org/PodcastEpisode JSON-LD, Open Graph and Twitter cards using the show's cover art, and a canonical URL.
  • A print stylesheet. Everything folded opens, the navigation and the player drop out, and the glossary is left off so a printout is the article.
  • A reading-progress bar, playback-speed buttons, and the light/dark toggle.

Two files it picks up on its own

Both are optional and both are already in the working directory by the time this step runs, so the command above does not mention them:

| File | What it gives the page | |---|---| | covered.md | previous/next links with real titles, and the archive | | glossary.md | the glossary section, filtered to episodes ≤ this one |

Written by publish_episode.py in step 2 — which is why the page is built after the publish, not before. Override with --covered PATH / --glossary PATH, or pass none to leave either out.

3b. Build the e-mail

The daily e-mail used to be written from scratch every morning, so it drifted. It is now rendered from the same epNN.md as the page:

python3 package/build_email.py --notes ep05.md --number 5 \
  --title "Wheat: The Map and the Screens" \
  --dek "One sentence under the headline." \
  --audio-url "https://.../ep05.mp3" \
  --page-url  "https://.../ep05.html" \
  --duration 739 --date "Friday 14 August 2026" \
  --charts "URL1,URL2,URL3" --glossary glossary_final.md \
  --drill-index 5 --drill-file conversions.md \
  --out ep05_email.html

Writes ep05_email.html and, from the same parse, the plain-text alternative ep05_email.txt. Send both parts.

  • --charts takes the published PNG URLs, comma-separated, in the order build_page.py printed its CHART_PNG= lines. Chart n in the notes gets URL n, so a pulse chart lands in the pulse and lesson charts land inline in the written edition. Missing URLs are skipped with a warning, never guessed.
  • --drill-index defaults to ((N − 1) mod 12) + 1, the rotation in conversions.md; pass it explicitly only to override the cycle.
  • --dek is optional and becomes the standfirst and the inbox preheader.

The section order is fixed and the builder does not let it move: header with the listen button → read online → market pulse with its chart → conversion drill → quiz → spoiler separator → solutions → the written edition → the cumulative glossary. Sections are found by their heading rather than their position, so the early episodes — which ordered their notes differently — come out in the same order as today's.

E-mail HTML is not web HTML, and the output obeys the difference: tables for layout, every style inline, a 600px card with the width attribute set (Outlook ignores max-width), no <style> block, no flexbox, grid, CSS variable or media query, charts as <img> with alt text and an explicit width, and a spoiler gap made of fixed-height table rows rather than stacked <br>, which Gmail collapses.

3c. Publish an essay

The site's Writing section reads writing/index.json out of the bucket.

python3 package/build_post.py --notes essay.md --slug basis-is-a-verb \
  --title "Basis Is a Verb" --dek "One sentence." \
  --date "14 August 2026" --outdir writing

Writes writing/basis-is-a-verb.html, upserts the entry in writing/index.json and rebuilds writing/index.html. Publish all three with publish_asset at those exact paths. Run it with no --notes to rebuild just the index. Essays use the same renderer and palette as the episode pages.

4. Charts

chart.py renders a small JSON spec two ways from the same layout: an inline SVG for the page, which inherits the page's CSS variables and therefore follows the light/dark toggle, and a PNG for the e-mail, because no mail client renders inline SVG. Types: line (add "mode":"index" to rebase series to 100), bar, waterfall.

Charts are written straight into epNN.md as fenced blocks:

```chart
{"type":"line","unit":"c/bu","title":"…","caption":"…","source":"…",
 "x":["4 Aug","5 Aug"],"series":[{"name":"Dec 26","values":[471,468]}]}
```

build_page.py picks them up automatically. Add --charts-prefix ep03_chart and it also writes ep03_chart1.png, ep03_chart2.png, printing CHART_PNG= for each, in document order. Publish those with the connector's publish_asset tool and pass the URLs to build_email.py --charts in the same order.

Two things worth knowing:

  • The SVG sizes its type in viewBox units, so it shrinks with the chart. A phone renders 640 units into about 360 pixels, which would turn 12px type into 7px, so the stylesheet inside the SVG scales the labels back up under a width media query — and on the page the figure breaks out to the full width of a small screen.
  • The PNG is only ever seen in an e-mail, scaled into a 522px column, so its type is set larger than the SVG's. Titles, captions and sources wrap onto as many lines as they need; the canvas grows to fit rather than clipping them.

Standalone: python3 package/chart.py spec.json --svg out.svg --png out.png. PNG output needs Pillow; everything else is standard library.

See STYLE.md for when a chart earns its place.

Output

  • Episode audio: https://cdn.jsdelivr.net/npm/@sdelsad/commodity-desk-daily@<version>/epNN.mp3
  • Podcast RSS feed: https://cdn.jsdelivr.net/npm/@sdelsad/commodity-desk-daily@latest/feed.xml

Notes

  • Voices: am_michael, bm_george, af_heart, am_fenrir (Kokoro).
  • ffmpeg must be on PATH for the mp3 conversion.
  • jsDelivr caches aggressively; a freshly published episode can take a few minutes to become fetchable, and podcast apps may see the feed later still.