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

@inferencesh/app

v0.5.3

Published

App framework for building inference.sh apps — File handling, output metadata, storage utilities

Readme

@inferencesh/app — build inference.sh apps in node.js

npm version License: MIT TypeScript

app framework for building inference.sh apps in node.js — file handling, output metadata, and storage utilities.

this is the app-side sdk. for the client-side sdk (calling apps, agents, file uploads), see @inferencesh/sdk.

installation

npm install @inferencesh/app

what's included

| export | description | |--------|-------------| | File | smart file reference — downloads urls, caches locally, resolves paths, serializes for the engine | | StorageDir | standard storage directory constants (DATA, TEMP, CACHE) | | download() | download a url to a directory with caching | | textMeta, imageMeta, videoMeta, audioMeta, rawMeta | output metadata factories for usage-based pricing | | createStreamSchema, pcm16, media, Live | live fields for stream functions — values that flow over the socket while the task runs | | meta() | per-function credentials and annotations (<Name>Meta exports) |

file handling

the File class handles downloading, caching, and path resolution — the node.js equivalent of the python sdk's File class.

reading input files

import { File } from "@inferencesh/app";

async run(inputData) {
  // Input files come as URLs — File downloads and caches them
  const file = await File.from(inputData.image);
  console.log(file.path);        // /home/.cache/inferencesh/files/abc123/image.jpg
  console.log(file.contentType);  // image/jpeg
  console.log(file.size);        // 102400
}

returning output files

import { File } from "@inferencesh/app";

async run(inputData) {
  const outputPath = "/tmp/result.png";
  await generateImage(inputData.prompt, outputPath);

  // File.fromPath is sync — no download needed for local files
  return { image: File.fromPath(outputPath) };
}

the engine reads path from the serialized output and uploads it to cdn automatically.

how File serializes

File implements toJSON() so it works seamlessly with JSON.stringify:

const file = File.fromPath("/tmp/output.png");
JSON.stringify(file);
// {"path":"/tmp/output.png","content_type":"image/png","size":102400,"filename":"output.png"}

construction options

// From URL (downloads and caches)
const file = await File.from("https://example.com/image.jpg");

// From local path
const file = await File.from("/tmp/output.png");

// From options object
const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });

// From engine-style data (snake_case)
const file = await File.from({ uri: "https://...", content_type: "image/jpeg" });

// Sync from local path (no download)
const file = File.fromPath("/tmp/output.png");

caching

downloaded files are cached at ~/.cache/inferencesh/files/{url_hash}/{filename}. set FILE_CACHE_DIR to override.

storage directories

import { StorageDir, ensureDir } from "@inferencesh/app";

// Standard directories available on inference.sh workers
StorageDir.DATA   // "/app/data"  — persistent storage
StorageDir.TEMP   // "/app/tmp"   — cleaned between runs
StorageDir.CACHE  // "/app/cache" — persists, may be evicted

// Ensure directory exists
const dataDir = ensureDir(StorageDir.DATA);

download utility

import { download, StorageDir } from "@inferencesh/app";

// Download to a specific directory with caching
const modelPath = await download(
  "https://huggingface.co/org/model/resolve/main/weights.bin",
  StorageDir.CACHE
);
// Returns: /app/cache/{hash}/weights.bin

skips download if the file already exists in the target directory (except for TEMP).

output metadata

report what your app processes and generates for usage-based pricing:

import { textMeta, imageMeta, videoMeta, audioMeta } from "@inferencesh/app";

// LLM app
async run(inputData) {
  const result = await this.llm.generate(inputData.prompt);
  return {
    response: result.text,
    output_meta: {
      inputs: [textMeta({ tokens: result.promptTokens })],
      outputs: [textMeta({ tokens: result.completionTokens })],
    },
  };
}

// Image generation app
async run(inputData) {
  const image = await this.model.generate(inputData.prompt);
  return {
    image: File.fromPath(image.path),
    output_meta: {
      outputs: [imageMeta({ width: 1024, height: 1024, steps: 20 })],
    },
  };
}

// Video generation app
async run(inputData) {
  return {
    video: File.fromPath(videoPath),
    output_meta: {
      outputs: [videoMeta({ resolution: "1080p", seconds: 5.0, fps: 30 })],
    },
  };
}

meta types

| factory | key fields | |---------|-----------| | textMeta | tokens | | imageMeta | width, height, resolution_mp, steps, count | | videoMeta | width, height, resolution, seconds, fps | | audioMeta | seconds, sample_rate | | rawMeta | cost (dollar cents) |

all meta types support an optional extra field for app-specific pricing factors.

full app example

import { z } from "zod";
import { File, textMeta } from "@inferencesh/app";

export const RunInput = z.object({
  prompt: z.string().describe("Input prompt"),
  image: z.string().optional().describe("Optional image URL"),
});

export const RunOutput = z.object({
  result: z.string(),
  processedImage: z.any().optional(),
});

export class App {
  async setup(config) {
    this.model = await loadModel();
  }

  async run(inputData) {
    // Handle input files
    let imageFile;
    if (inputData.image) {
      imageFile = await File.from(inputData.image);
    }

    const result = await this.model.process({
      prompt: inputData.prompt,
      imagePath: imageFile?.path,
    });

    return {
      result: result.text,
      processedImage: result.outputPath
        ? File.fromPath(result.outputPath)
        : undefined,
      output_meta: {
        inputs: [textMeta({ tokens: result.inputTokens })],
        outputs: [textMeta({ tokens: result.outputTokens })],
      },
    };
  }
}

stream functions

a stream function takes its input and a socket (async talk(inputData, socket)), runs until it returns, and its return value is the result. some input and output fields are live: their values travel over the socket while the task runs instead of in the request body. declare them with createStreamSchema; in the json schema they become {"type": "array", "format": "stream", "items": ...} and are never required.

import { z } from "zod";
import { createStreamSchema, pcm16, Live } from "@inferencesh/app";

export const TalkInput = z.object({
  effect: z.enum(["none", "robot"]).default("none"),   // ordinary: set at the start, may change live
  audio: createStreamSchema(z, pcm16(z, 16000)),      // live media: binary frames
});

export const TalkOutput = z.object({
  audio: createStreamSchema(z, pcm16(z, 16000)),
  frames: z.number().default(0),
});

export class App {
  async talk(input, socket) {
    const live = new Live(socket, input, TalkInput, TalkOutput);
    let n = 0;
    for await (const { field, value } of live) {
      if (field === "audio") {          // value is a Buffer of PCM samples
        n++;
        await live.send({ audio: value });
      }
      // an ordinary field (effect) is already set on input when it changes
    }
    return { frames: n };
  }
}

on the wire, a binary frame is one item of the schema's single binary live field (pcm16(z, rate, channels) or media(z, contentType)); a json text frame is a partial object keyed by field name: an item of a live field ({"events": {"type": "interrupt"}}) or a new value for an ordinary field ({"effect": "robot"}). frames that do not fit the input schema are answered with {"error": {"field", "message"}} and skipped. live.send({...}) sends the output binary field as a binary frame and everything else as one json frame; unknown output fields throw.

progress from a stream function

a stream function may also be an async generator (async *dictate(input, socket)). each yield is a progress snapshot of the task's output: it replaces the task's output and reaches clients as a task update (sse /tasks/{id}/stream), not over the socket — the socket carries the live fields. yield cumulative snapshots; the last yield is the result, so output_meta goes there, and a generator that ends without yielding fails the task (NoOutput).

export const DictateInput = z.object({
  audio: createStreamSchema(z, pcm16(z, 16000)),   // mic frames arrive over the socket
});

export const DictateOutput = z.object({
  text: z.string().default(""),
  partial: z.boolean().default(true),
});

export class App {
  async *dictate(input, socket) {
    const live = new Live(socket, input, DictateInput, DictateOutput);
    let window = Buffer.alloc(0);
    for await (const { field, value } of live) {
      if (field === "audio") {
        window = Buffer.concat([window, value]);
        if (window.length >= 32000) {                       // one second of 16 khz s16le
          yield { text: await this.transcribe(window) };    // the transcript so far
        }
      }
    }
    yield {
      text: await this.transcribe(window),
      partial: false,
      output_meta: { inputs: [], outputs: [] },
    };
  }
}

the client closing the socket ends the for await, so the final yield goes after the loop. both channels can be used at once: live.send({ text }) pushes a partial over the socket for the lowest latency, while the yields build the transcript the task stores.

frames dropped when the app falls behind

binary frames are perishable media, so the kernel keeps at most 256 of them queued for the app (about five seconds of 20 ms audio) and drops the oldest when the app reads slower than the client sends — latency stays bounded instead of growing for ever. text frames (commands and patches) are never dropped. socket.dropped counts the binary frames lost so far, and socket.binaryBacklog = null turns dropping off when every frame matters (a file sent in chunks).

per-function credentials and annotations

export <PascalName>Meta next to a function's input and output schemas. credentials narrows what the function needs at run time (each entry must also be in inf.yml's credentials:; a function without it gets the app-wide list). annotations are hints about what it does.

import { meta } from "@inferencesh/app";

export const CreateEventInput = z.object({ title: z.string() });
export const CreateEventOutput = z.object({ id: z.string() });
export const CreateEventMeta = meta({
  credentials: [{ key: "google.calendar.events" }],
  annotations: { readOnlyHint: false, destructiveHint: false },
});

export class App {
  async createEvent(input) { /* ... */ }
}

meta() returns its argument unchanged after the same checks as sdk-py's @function (it throws on an empty credentials list, unknown keys, wrong value types, or a credential with neither key nor provider). it gives editors the CredentialRequirement and ToolAnnotations types (add // @ts-check to a .js file to get errors, not only completions).

requirements

  • node.js 18.0.0 or higher
  • zero runtime dependencies

resources

license

MIT © inference.sh