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

@crediolabs/app

v0.2.0

Published

Framework-agnostic browser client for Tuan mini-apps (/_api/*). Zero dependencies.

Readme

@crediolabs/app

Framework-agnostic browser client for Tuan mini-apps. Zero runtime dependencies. Talks to your app's /_api/* surface — authenticate end-users, store per-user data and files, and stream turns from the app's agents.

This file is read by both humans and AI coding agents. Pair it with the bundled type declarations (dist/app.d.ts) for the exact API surface.

Install

Build apps — install from npm:

npm install @crediolabs/app
import { createClient } from "@crediolabs/app";

No-build apps served by the harness can skip npm and import the embedded copy:

<script type="module">
  import { createClient } from "/_sdk/app.js";
  const app = createClient();
  await app.auth.anon();
  await app.data.set("hello", { at: Date.now() });
  console.log(await app.agent.run("hi").text());
</script>

Create a client

// Same-origin (app served from its own published domain): no options needed.
const app = createClient();

Cross-origin (host the frontend elsewhere, e.g. Cloudflare Pages)

Point the client at your app's API domain and authorize the frontend origin. baseUrl rewrites every /_api/* call and the WebSocket URL.

const app = createClient({ baseUrl: "https://your-app.tuan.example" });

For this to work, add the frontend's origin to the app's allowed origins (in the app's settings). CORS is default-deny; wildcards match exactly one DNS label, e.g. https://app-*.example.com. Auth is a Bearer token (no cookies), so no credentials mode is needed.

Authentication

Each end-user gets a per-origin token (persisted in localStorage, key tuanapp:token). Pick the mode the app is configured for:

await app.auth.anon();            // anonymous apps: mint a fresh uid + token
await app.auth.passcode("1234");  // passcode apps: exchange a code for a token

const me = await app.auth.whoami();   // { uid, mode: "anonymous" | "passcode" }
app.auth.token;                       // current token (string | null)
app.auth.signOut();                   // clear the stored token

All other calls require a token; call one of the auth methods first.

Data (per-user key/value)

JSON values scoped to the current (app, uid). Isolated per user.

await app.data.set("profile", { name: "Ada" });
const profile = await app.data.get<{ name: string }>("profile"); // null if absent
const keys = await app.data.list();
await app.data.delete("profile");

Artifacts (per-user files)

Files scoped to the current (app, uid). Both end-user uploads and agent-presented files land here.

const { id } = await app.artifacts.upload(file, { name: "report.csv" }); // file: Blob/File
const all = await app.artifacts.list();   // ArtifactMeta[]
const one = await app.artifacts.get(id);  // ArtifactMeta | null

// Rendering an image artifact in <img>:
const url = await app.artifacts.getBlobUrl(id); // authed fetch -> object URL
imgEl.src = url;
// NOTE: artifacts.fileUrl(id) returns the bare URL with NO token — it will NOT
// load in a bare <img src>. Use getBlobUrl() for that.

ArtifactMeta = { id, name, mediaType, size, createdAt }.

Agent

Run a turn, stream events, or read history. agent defaults to the app's configured default agent.

Run a turn

// One-shot text:
const reply = await app.agent.run("Summarize my latest upload.").text();

// Or stream events as they arrive (async iterable):
for await (const ev of app.agent.run("Write a haiku.")) {
  if (ev.type === "delta") process.stdout.write(ev.text);
}

Attach files to a turn

Upload first, then pass artifact ids. The agent reads the file with its own tools. Any file type; max 5 per turn; you may only attach your own artifacts.

const { id } = await app.artifacts.upload(file, { name: "data.csv" });
const answer = await app.agent
  .run("What is the total in the attached CSV?", { attachments: [id] })
  .text();

The app's agent must have a file-reading tool (e.g. Read) in its allowlist, or it cannot open the attachment.

Event types

agent.run(...) and agent.stream(...) emit AgentEvent:

| type | payload | meaning | |--------------|--------------------------------------|----------------------------------| | turn.start | — | the turn began | | delta | { text } | a chunk of the assistant reply | | message | { role: "assistant", content, id } | the finalized assistant message | | artifact | { artifact: ArtifactMeta } | the agent presented a file (now in artifacts.list()) | | turn.end | — | the turn finished | | error | { error } | the turn errored |

Handle a presented file live (no polling needed):

for await (const ev of app.agent.run("Make me a chart.")) {
  if (ev.type === "artifact") {
    const src = await app.artifacts.getBlobUrl(ev.artifact.id);
    // show ev.artifact.name / src
  }
}

Observe the agent over a WebSocket

stream opens a read-only socket for the agent's events (the turn itself is started by run). Returns a handle with close().

const sub = app.agent.stream({}, (ev) => {
  if (ev.type === "artifact") {/* ... */}
});
// later:
sub.close();

History

const msgs = await app.agent.history(); // AgentMessage[] (user + assistant only)

Errors

Failed calls throw AppError with a status (HTTP code) and a stable code (the server's error string, e.g. unauthorized, unknown_artifact, too_many_attachments, quota_exceeded, busy).

import { AppError } from "@crediolabs/app";

try {
  await app.data.get("x");
} catch (e) {
  if (e instanceof AppError && e.status === 401) await app.auth.anon();
  else throw e;
}

Notes

  • One app per origin. The token store has no app id in its key — a given origin serves exactly one app.
  • Per-user isolation. Data, artifacts, and agent history are scoped to the authenticated uid; a user can never see another user's.
  • Advanced createClient options: token (seed an existing token), fetch / WebSocket (inject implementations for SSR/testing), storage (custom token persistence; defaults to localStorage, falls back to in-memory).