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

@microclimates/envos-app

v0.1.1

Published

Build Node.js apps for EnvOS: a typed client, live card updates, triggers, test helpers, and a scaffolder made for AI-assisted coding.

Readme

envos-app

Build Node.js apps for EnvOS, the Microclimates environmental control platform.

Read live sensor data, react to changes as they happen, run tasks and drive controls, from any Node.js program. envos-app gives you a typed client, live card updates, triggers, safety rails for writes, a mock site for tests, and a project template that AI coding tools understand.

npm CI license

import { defineApp, onThreshold } from "@microclimates/envos-app";

export default defineApp({
  name: "frost-watch",
  triggers: [
    onThreshold({ category: "monitor", field: "state.out.temp", below: 38, hysteresis: 2 }, ({ card, state, value }, ctx) => {
      ctx.log.warn(`${card.config.title}: frost risk ${state} at ${value}°F`);
    }),
  ],
});

Contents

Quick start

You need Node.js 22.18 or later, your site's URL (e.g. https://c1234-abcd.microclimates.com), and an API token for it.

npx @microclimates/envos-app create my-app
cd my-app
npm install
cp .env.example .env        # paste your token
npm run doctor              # checks URL, token and live connection
npm run dev                 # runs the starter app

The starter app watches every monitor on the site and logs health changes and high humidity. It only reads, so a view token is enough.

Using envos-app in an existing project

npm install @microclimates/envos-app
npx envos-app add-site main --url https://c1234-abcd.microclimates.com --auth view
echo "ENVOS_MAIN_TOKEN=<token>" >> .env
import { EnvosClient } from "@microclimates/envos-app";

const client = await EnvosClient.fromConfig();
const site = client.site("main");

for (const card of await site.cards.list({ filter: 'component.category="monitor"' })) {
  console.log(card.config.title, card.state.out.health);
}
await client.close();

For a one-off script you can skip envos.json: set ENVOS_URL, ENVOS_TOKEN and optionally ENVOS_AUTH, and EnvosClient.fromConfig() uses a single site named default.

How EnvOS is organized

Everything on a site is a card: a sensor, a relay, a climate monitor, an irrigation program.

| Part | Meaning | |---|---| | card.component | The class: id, category, field definitions with labels and units, tasks. Read-only. | | card.config | How this card is set up: title, zone, tags, class-specific settings, outbound wires. | | card.state.in | What you ask the card to do. | | card.state.out | What the card reports. Only the card writes it. |

Wires carry one card's state.out value into another card's state.in. The component.category tells you where a card sits on the signal path:

device  →  monitor  →  automation  →  control  →  device
 (how)      (what)      (decide)       (what)      (how)

Apps that act behave like automations: they read monitors and drive controls, and EnvOS stages the hardware. The full model is in agents/envos-concepts.md.

Connecting to sites

Sites live in envos.json, which holds no secrets:

{
  "$schema": "./node_modules/@microclimates/envos-app/schema/envos.schema.json",
  "sites": {
    "north": { "url": "https://c1234-abcd.microclimates.com", "auth": "control", "description": "North greenhouse" },
    "lab":   { "url": "https://192.168.10.20:8443", "auth": "view", "dryRun": true }
  }
}

| Key | Meaning | |---|---| | url | Site URL. The developer portal's …/api/v2 form works too. | | auth | The level the site's token was issued with: view, control, config or admin. envos-app refuses writes above it before sending them. | | tokenEnv | Variable holding the token. Default ENVOS_<NAME>_TOKEN. | | dryRun | Log and audit writes without sending them. | | deviceWrites | warn (default), deny or allow inputs and tasks sent straight to device cards. | | description | For people and coding agents. |

Tokens come from the environment or .env. Each token level includes the ones before it:

| Level | Can | |---|---| | view | Read cards, schemas, history, events, wires | | control | Also send inputs and run tasks | | config | Also change card configuration | | admin | Everything. Apps should not need it. |

const client = await EnvosClient.fromConfig();
client.site("north");     // one site
client.sites();           // all of them

See docs/configuration.md.

Reading data

Each site groups the REST API by area. Every method is typed and documented in your editor.

const site = client.site("north");

await site.health();                                                  // { envosVersion }
await site.cards.list({ filter: 'state.out.health!="ok"', include: ["id", "config.title"] });
await site.cards.get(id);                                             // complete card
await site.cards.getMany([id1, id2]);
await site.schemas.get(id);                                           // JSON Schema: fields, units, ranges

await site.history.metrics(id);                                       // what the card keeps
await site.history.series(id, { period: "last_24h", metrics: ["temp"], interval: "15m" });
await site.history.summary(id, { period: "last_7d" });                // min/max/avg/trend + narrative
await site.history.seriesCsv(id, { period: "last_30d" });

await site.events.list(id, { period: "last_24h", level: "error" });
await site.wires.list({ card: id });                                  // edges in and out, resolved

Errors are typed and carry a stable code (ENVOS_AUTH, ENVOS_NOT_FOUND, ENVOS_BAD_REQUEST, ENVOS_NETWORK …). Reads retry on network errors and 502/503/504. See agents/troubleshooting.md.

Live data

EnvOS pushes every card change as it happens. Connect once and envos-app keeps every card in memory:

await site.connect();

site.live.get(id);                                     // instant, no request
site.live.list({ category: "monitor", zone: "Bay 1" });

const stop = site.onCardChange((card, prior, { changed }) => {
  console.log(card.config.title, changed, prior?.state.out.humid, "→", card.state.out.humid);
}, { category: "monitor", fields: ["state.out.humid"] });
  • Handlers receive their own copies of cards; the shared class definitions cannot be altered.
  • Dropped connections reconnect automatically, and changes missed in between are delivered on reconnect. Watch site.onConnectionChange() to pause decisions while reconnecting.
  • Select cards with cardId, category, componentId, zone, tag or a where function.

Apps and triggers

defineApp describes an app; runApp loads envos.json, connects the sites the triggers use, runs setup, starts the triggers, and stops cleanly on Ctrl+C.

// src/app.ts
import { defineApp, onCardChange, onHealthChange, onThreshold } from "@microclimates/envos-app";

export default defineApp({
  name: "bay-watch",

  setup(ctx) {
    ctx.log.info(`${ctx.site("north").live.size} cards loaded`);
  },

  triggers: [
    onHealthChange({ site: "north", category: "monitor" }, ({ card, priorHealth, health }, ctx) => {
      ctx.log.warn(`${card.config.title}: ${priorHealth} → ${health}`);
    }),

    onThreshold({ site: "north", cardId: bay1, field: "state.out.humid", above: 85, hysteresis: 3 }, (event, ctx) => {
      ctx.log.warn(`humidity ${event.state}`, { value: event.value });
    }),

    onCardChange({ site: "north", category: "control", fields: ["state.out.value"] }, (card, prior, ctx) => {
      ctx.log.info(`${card.config.title} now at ${card.state.out.value}%`);
    }),
  ],
});
// src/main.ts
import { runApp } from "@microclimates/envos-app";
import app from "./app.ts";
await runApp(app);

| Trigger | Fires | |---|---| | onCardChange(selector, (card, prior, ctx) => …) | On every change to a matching card, optionally only when fields change | | onThreshold({ field, above?, below?, hysteresis? }, (event, ctx) => …) | Once when a value crosses a limit and once when it returns past the hysteresis band; also at startup if already past | | onHealthChange(selector, (event, ctx) => …) | When state.out.health moves between ok, warn and alert |

Handler errors are logged, never thrown into the stream.

Writing safely

EnvOS moves real equipment, so every write goes through the same checks:

await site.cards.setInput(foggerId, { value: 40 });            // control: ask a card to do something
await site.cards.queueTask(valveId, "Irrigate Plants");        // control: start a task with defaults
await site.cards.runTask(valveId, { key: "irrigate", name: "Irrigate Plants", inputs: { mins: 20 } });
await site.cards.stopTask(valveId, "Irrigate Plants");
await site.cards.setRunning(cardId, false);                    // take out of service
await site.cards.setConfig(cardId, { notes: "Replaced sensor" }); // config
  1. Declared auth level. A write above the site's auth throws WriteDeniedError without sending anything.
  2. Device policy. Inputs sent straight to device cards log a warning (or are refused with deviceWrites: "deny"). Drive control cards instead.
  3. Dry run. With dryRun: true, writes are logged and audited, not sent.
  4. Audit. Every attempt (sent, dry run, denied, failed) is recorded. Add jsonlAuditSink("audit/writes.jsonl") for a file.
  5. Requests, not results. Writes resolve with queued: true; the card decides what happens. Read state.out to confirm.

Writes are never retried. See agents/safety-rules.md.

Testing

@microclimates/envos-app/testing runs a fake EnvOS site on localhost with the same REST API, errors, auth behavior and live stream, so apps can be tested without hardware or tokens.

import { EnvosClient, runApp } from "@microclimates/envos-app";
import { SAMPLE_IDS, sampleSite, startMockEnvos } from "@microclimates/envos-app/testing";
import app from "../src/app.ts";

test("boosts the fogger when humidity drops", async () => {
  const mock = await startMockEnvos({ cards: sampleSite(), auth: "control" });
  const client = new EnvosClient({ sites: { north: { url: mock.url, token: mock.token, auth: "control" } } });
  const running = await runApp(app, { client, handleSignals: false });

  mock.updateCard(SAMPLE_IDS.climate, { out: { humid: 48 } });
  await expect.poll(() => mock.writes).toContainEqual(expect.objectContaining({ cardId: SAMPLE_IDS.fogger }));

  await running.stop();
  await client.close();
  await mock.close();
});
  • sampleSite(): a small greenhouse along the full signal path (weather station, climate monitor, recipe, fogger control, relay board).
  • cardFixture({ id, category, out, … }): your own cards with your real field names.
  • mock.updateCard(), addCard(), removeCard(), dropConnections(): drive the site.
  • mock.writes, mock.requests: assert on what your app did.
  • waitForCard(site, id, predicate): wait for live data to reflect something.

Building with AI coding tools

Every project made by envos-app create includes:

  • AGENTS.md, read by Claude Code, Cursor, Copilot, Codex and others. It covers the commands, the EnvOS model, the API, and the rules. It also has a generated table of the project's sites with each token's auth level, so the agent knows what it may change.
  • Skills in .claude/skills/: inspect a site, add a trigger, add a site.
  • scripts/inspect-card.ts, which prints a card's real fields, units, tasks, metrics and wires, so agents look up names instead of guessing them.
  • Guides shipped in the package under node_modules/@microclimates/envos-app/agents/, always matching the installed version: concepts, cookbook, safety rules, troubleshooting.

Then ask for what you want: "Alert me by email when any bay's temperature stays above 90°F for 10 minutes", "Add our south greenhouse as a second site", "Show a daily summary of humidity per zone." The agent writes the trigger, the test, and tells you what it will write and where.

CLI

| Command | Does | |---|---| | npx @microclimates/envos-app create [dir] | New project from a template (--url, --auth, --site, --template, --yes) | | npx envos-app add-site <name> --url <url> --auth <level> | Add or update a site in envos.json, .env.example and AGENTS.md | | npx envos-app doctor [--site name] | Check URL, token variable, reachability, token, live stream | | npx envos-app cards [--site name] [--filter expr] [--json] | List the cards on a site |

doctor and cards read .env from the project.

Developer portal

The EnvOS developer portal lets you explore a site's API interactively. envos-app covers the same operations one to one. @microclimates/envos-app/portal exports the operation list and generates envos-app code for any request made in the portal:

import { envosAppSnippet } from "@microclimates/envos-app/portal";

envosAppSnippet({ method: "GET", path: "/cards/:id/history", params: { id }, query: { period: "last_24h" } });

A test checks this list against the EnvOS OpenAPI contract, so the package, the portal and EnvOS cannot drift apart unnoticed. See docs/developer-portal.md.

Requirements

  • Node.js 22.18 or later.
  • An EnvOS site with REST API v2, and an API token for it.

More

License

MIT © Microclimates, Inc.