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

glove-env-slides

v1.0.0

Published

Slides stdlib adapter for glove-working-environment. Builds PowerPoint decks and reads them back as env:slides — create, describe, extract text and speaker notes, outline. Paths in, paths out.

Readme

glove-env-slides

PowerPoint decks for glove-working-environment. Registers as env:slides: an agent builds a deck from a spec, or reads one it was handed.

pnpm add glove-env-slides
import { createWorkingEnvironment } from "glove-working-environment";
import { slides } from "glove-env-slides";

const env = await createWorkingEnvironment({ stdlib: [slides()] });

Building

import { create } from 'env:slides';

export default async function main() {
  return create({
    title: 'Q3 Review',
    subtitle: 'Prepared for the board',
    footer: 'Confidential',
    slides: [
      { title: 'Headline', metric: { value: '$4.2M', caption: 'revenue, up 12% QoQ' } },
      { title: 'By region', table: [['Region', 'Revenue'], ['EMEA', '$1.8M'], ['AMER', '$2.4M']] },
      {
        title: 'What changed',
        bullets: ['EMEA renewals landed early', '  two of them slipped from Q2'],
        notes: 'The board asked about the Q2 slip last time — lead with it.',
      },
    ],
  }, '/out/q3.pptx');
}

A slide takes one kind of content — bullets, table, metric or body, applied in that order — plus an optional image (a VFS path, so generate it with env:images first) and notes. Indent a bullet by prefixing two spaces.

A table longer than a slide continues onto further slides with its header repeated, rather than running off the bottom. That matters more than it sounds: an overflowing table still puts every row in the file, so extract() finds them all and nothing looks wrong until someone opens the deck.

The palette and layout are fixed rather than configurable. An agent choosing colours per deck produces something worse than a consistent default, and every knob is a decision it has to spend a turn on.

When the spec is not enough

create builds a good standard deck and cannot build a different one — a two-column slide, a chart, particular colours and positions. So pptxgenjs is exported as-is:

import { PptxGenJS } from 'env:slides';

export default async function main() {
  const pptx = new PptxGenJS();
  pptx.layout = 'LAYOUT_16x9';                 // 10 × 5.63 inches; positions are inches
  const s = pptx.addSlide();
  s.addText('By region', { x: 0.6, y: 0.4, w: 8.8, fontSize: 26, bold: true, color: '1A1A2E' });
  s.addShape(pptx.ShapeType.rect, { x: 0.6, y: 1.1, w: 1.2, h: 0.06, fill: { color: '2563EB' } });
  s.addTable(rows, { x: 0.6, y: 1.5, w: 4.2, autoPage: true, autoPageRepeatHeader: true });
  s.addImage({ path: '/tmp/chart.png', x: 5.2, y: 1.5, w: 4.2, h: 3.3 });
  s.addNotes('Revenue is concentrated in EMEA.');
  return pptx.writeFile({ fileName: '/out/q3.pptx' });
}

That is pptxgenjs, verbatim from its own documentation — which is the point. Models have read thousands of examples of this shape, and an API that differs makes them translate.

Everything is synchronous until writeFile, the only await and the only thing that produces a file. The library's own writeFile is replaced, not wrapped: bytes are produced in memory and land in the VFS through the guarded handle. addImage({ path }) resolves through the VFS too — left alone, pptxgenjs would open that path on the host filesystem.

One thing that does not carry over: you cannot read values back off the deck mid-build. The whole recording replays at the write, so there is nothing to return; interpolating a builder yields a label saying so rather than throwing.

Reading

describe() answers "what am I holding?" for a few dozen tokens — no deck is small enough to read blind:

const summary = await describe('/inbox/board.pptx');
// { format: 'pptx', slides: 31, titles: [...], words: 2140, media: 6, bytes: 284102 }

extract() returns every slide's title, body paragraphs and speaker notes. outline() flattens the whole deck to markdown with ## Slide N: Title headers — write that to a file and grep it, which costs a fraction of pulling 31 slides through the response cap.

Editing, not regenerating

create() writes a new deck. replaceText() changes one that already exists — 'fix the typo on slide 4' — and the difference is the whole point:

await replaceText('/inbox/q3.pptx', { 'Nortwind': 'Northwind' }, { slides: 4 });
// → { path, replacements: 1, slides: [{ slide: 4, replacements: 1 }], unmatched: [] }

Only the slide parts holding the matched text are rewritten. Every other part — masters, layouts, theme, ppt/media/*, animations, the slides you did not scope to — is copied across still compressed, so code that never decoded a part cannot change it. Measured on a deck this adapter wrote: the edit rewrote one of 50 parts and left the other 49 byte-identical, where reading the text out and rebuilding the deck lost the chart image and the footer's slide layout. On a deck a designer made, the rebuild loses the design.

pptxgenjs cannot open a .pptx, so this goes through the same independent OOXML path as the reader: inflate the part, splice its text runs, re-emit. Text split across formatting runs is still found — runs are joined per paragraph before matching — and a replacement lands in the run its match started in, keeping that run's font, size and colour. A search that matches nothing throws rather than writing an identical deck.

Anything beyond replacing strings — adding a slide, moving one, changing a layout — still means building a new deck.

Why the reader is not pptxgenjs

Writing goes through pptxgenjs; reading goes through this package's own ZIP + OOXML reader in src/pptx.ts, and that asymmetry is deliberate.

Verifying a writer with its own library proves only that it is self-consistent. A title written into the wrong placeholder, or a bullet silently dropped, round-trips perfectly through the library that made the mistake. Opening the file independently is what catches it — and it is the only way to read a deck this environment did not write, which is most of the review work an agent is actually asked to do.

Two things that reader gets right and a naive one does not:

  • Runs are joined within a paragraph. PowerPoint splits one visual line into several <a:t> runs wherever formatting changes, so "Revenue grew 12%" is three runs. Joining on run boundaries turns one bullet into three and every count downstream is wrong.
  • Footers live on the slide master, not the slide. A footer stamped into each slide's own XML comes back as a body line on every slide, so a 30-slide deck yields 30 copies of "Confidential" and any summary built from the text inherits them. Chrome is not content.
  • Speaker notes are resolved through relationships, not by number. slide7.xmlnotesSlide7.xml holds for decks this package writes and for little else: PowerPoint numbers notes parts in creation order, so a deck where slide 2 got notes first has notesSlide1.xml hanging off slide 2. The rels part is the answer the file itself gives; the numeric convention survives only as the fallback for a deck with no rels at all.

The test suite reads decks back through that independent path, and one test skips both readers entirely — it runs the system unzip to CRC-check the archive and assert every OOXML part the spec requires is present, so a deck only our own code can open still fails.

Handling

Claims .pptx by extension only. A pptx is a ZIP and so are .docx and .xlsx, so the PK signature cannot tell them apart — claiming it would steal every Office file from the adapter that owns it. describe() verifies by looking for ppt/slides/ inside and says so when the container is some other kind of Office file.

Limits

  • ZIP64 archives are refused with a message rather than mis-parsed.
  • Charts are not generated. Render one with env:images and place it via image.
  • Reading recovers text, notes and media counts — not layout, animation or theming. It is built for review, not for round-tripping someone else's design.
  • Editing replaces text, and only text. It preserves layout, animation and theming by never touching them, which is a different guarantee from understanding them: replaceText cannot add a slide, resize a shape or restyle a run, and matching is literal, case-sensitive and confined to one paragraph.