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

@isomorph.ai/app-sdk

v1.11.0

Published

Use upstream `[email protected]` and `@ai-sdk/[email protected]` directly in actions. The platform supplies the gateway connection and approved aliases; no model wrapper is needed. Never import the provider setup into frontend code.

Downloads

4,022

Readme

Isomorph app SDK: M1 server AI and browser reads

Use upstream [email protected] and @ai-sdk/[email protected] directly in actions. The platform supplies the gateway connection and approved aliases; no model wrapper is needed. Never import the provider setup into frontend code.

// actions/summarise.ts
import { generateText, tool, jsonSchema, stepCountIs } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createClient } from '@isomorph.ai/app-sdk';

export default async function summarise() {
  const gateway = createOpenAI({
    baseURL: process.env.OPENAI_BASE_URL,
    apiKey: process.env.OPENAI_API_KEY,
  });
  const isomorph = createClient();
  const result = await generateText({
    model: gateway.chat(process.env.ISOMORPH_AI_GENERATION_MODEL_ALIAS!),
    prompt: 'Summarise the approved channel.',
    stopWhen: stepCountIs(5),
    tools: {
      readChannel: tool({
        description: 'Read the approved team channel.',
        inputSchema: jsonSchema<Record<string, never>>({ type: 'object', properties: {}, additionalProperties: false }),
        execute: () => isomorph.integrations.execute('company-slack', {
          operation: 'slack.channel.history', resource: 'APP_APPROVED_CHANNEL', input: { limit: 10 },
        }),
      }),
    },
  });
  return { text: result.text };
}

Replace the connection/resource with the app's existing approved integration. Tools inherit the invoking person's signed identity. Expose read operations only; existing SDK write methods are not M1 agent tools.

For images call generateImage({model: gateway.image(process.env.ISOMORPH_AI_IMAGE_MODEL_ALIAS!), prompt: 'A blue notebook'}). Editing uses prompt: {text: 'Make it red', images: [inputBytes]} only when isomorph ai models advertises edit. Reuse app files upload/signed reads for authorized input and output storage; respect existing transfer/action limits and never return credentials.

Company integrations

isomorph.integrations.execute(connection, request) is one method over a closed set of operations; the operation fixes whose identity it runs under, and the contract's bounds are checked in the client before any request is sent (the same sentence the platform answers with). The connection, operation, resource and mode are literals in the call: the kit gate derives .isomorph/integrations.json from them. isomorph dev without --real answers every operation from a local fixture.

Slack. A history, post, edit or delete runs as the app or as the signed-in person (mode, the one IT approved for the app); a DM and a user lookup run as the app only, against the fixed workspace resource. A post or DM needs a fresh UUID per explicit Send press.

// A thread reply in Slack mrkdwn
const page = await isomorph.integrations.execute('company-slack', {
  operation: 'slack.channel.history', resource: 'team-updates', input: { limit: 100 },
});
await isomorph.integrations.execute('company-slack', {
  operation: 'slack.message.post', resource: 'team-updates',
  input: { text: '*Done* — see <https://example.com/report|the report>', format: 'mrkdwn', threadId: page.messages[0].id },
  idempotencyKey: crypto.randomUUID(), mode: 'app',
});

// Paged history: hasMore and nextCursor say when and how to read the next (older) page
const older = await isomorph.integrations.execute('company-slack', {
  operation: 'slack.channel.history', resource: 'team-updates', input: { limit: 100, cursor: page.nextCursor },
});

// Edit or delete a message the app posted (its ts is the messageId)
const posted = await isomorph.integrations.execute('company-slack', {
  operation: 'slack.message.post', resource: 'team-updates', input: { text: 'Deploy starting' }, idempotencyKey: crypto.randomUUID(), mode: 'app',
});
await isomorph.integrations.execute('company-slack', {
  operation: 'slack.message.update', resource: 'team-updates', input: { messageId: posted.messageId, text: 'Deploy finished' }, mode: 'app',
});
await isomorph.integrations.execute('company-slack', {
  operation: 'slack.message.delete', resource: 'team-updates', input: { messageId: posted.messageId }, mode: 'app',
});

// A direct message by email (1 to 8 people; several open one group DM) and a user lookup
await isomorph.integrations.execute('company-slack', {
  operation: 'slack.dm.send', resource: 'workspace', input: { to: ['[email protected]'], text: 'Your review is due today.' },
  idempotencyKey: crypto.randomUUID(),
});
const { userId } = await isomorph.integrations.execute('company-slack', {
  operation: 'slack.user.lookup', resource: 'workspace', input: { email: '[email protected]' },
});

Gmail runs as the signed-in person after their consent, on the fixed inbox resource.

// Search, then read a whole thread
const found = await isomorph.integrations.execute('company-gmail', {
  operation: 'gmail.thread.list', resource: 'inbox', input: { q: 'from:jane newer_than:7d', limit: 50 },
});
const thread = await isomorph.integrations.execute('company-gmail', {
  operation: 'gmail.thread.read', resource: 'inbox', input: { threadId: found.threads[0].id },
});
// found.nextPageToken, when hasMore, is the next read's pageToken

// An attachment: a message or thread read names them (id, filename, mimeType, size); this fetches the bytes
const file = thread.emails[0].attachments?.[0];
if (file) {
  const { dataBase64 } = await isomorph.integrations.execute('company-gmail', {
    operation: 'gmail.attachment.read', resource: 'inbox', input: { messageId: thread.emails[0].id, attachmentId: file.id },
  });
}

// An HTML send: text stays the plain-text alternative; up to 5 attachments of 2 MiB each
await isomorph.integrations.execute('company-gmail', {
  operation: 'gmail.message.send', resource: 'inbox',
  input: {
    to: ['[email protected]'], subject: 'Weekly report', text: 'The report is attached.',
    html: '<p>The report is <b>attached</b>.</p>',
    attachments: [{ filename: 'report.csv', contentType: 'text/csv', dataBase64: csvBase64 }],
  },
  idempotencyKey: crypto.randomUUID(),
});

(1.8.0; contracts 0.71.0)

Browser read tools

Install optional peers ai and [email protected]. Locally run npx playwright-core install chromium --only-shell; V2 packages Chromium in the existing app image. An existing system executable can be supplied as executablePath.

import { createBrowserTools } from '@isomorph.ai/app-sdk/dist/browser.js';
// Inside an action with its context:
const browser = await createBrowserTools({
  pages: ['https://example.com/'],
  signal: context.signal,
});
try {
  // Add browser.tools to generateText's tools; keep stopWhen: stepCountIs(5).
} finally {
  await browser.close();
}

App code declares exact HTTPS pages and named click/fill selectors reviewed for reads. Tools provide navigate, observe, control, scroll, screenshot and close. Observations are untrusted webpage content. One isolated page per run, one active run per process, 50 actions, five minutes, ten-second operations, bounded text/screenshots. Private destinations, undeclared navigation, downloads, popups, WebSockets, service workers and non-GET/HEAD requests are blocked. GET-only filtering cannot infer website business semantics: never declare URLs/controls that perform external writes. Chromium's sandbox remains enabled; unsupported sandbox environments fail clearly.

Running and rollout

isomorph dev uses canned server-model fixtures. dev --real supplies an approved development connection to the action process. Hosted actions resolve their exact ACTIVE version key. The current V2 PLAN is published after LIVE; before key activation, an AI action fails and later actions retry. LIVE alone is not AI acceptance.

Requires compatible SDK/CLI/toolkit publication, gateway protocol fixes, governance connection routes and V2 packaging/version wiring. No deployment is claimed here. Real model/file authorization and hosted Chromium sandbox checks remain release acceptance. Search, company skills, integration/browser writes, video, audio, STT, TTS and realtime audio are deferred.

Where the browser client sends its calls

createClient() with no baseUrl picks its base URL like this:

  • In Node (the retained-check runner, the kit runtime), ISOMORPH_GATEWAY_URL.
  • In a browser, window.__isomorphApiOrigin when it is an http(s) origin string such as https://fourier-app-x.api.preview.beta.apps.isomorph.ai. A deployed kit page carries the platform's credential shim, a <script> in <head> that sets this global to the app's own API host and wraps window.fetch so calls there carry the sign-in cookie; the realtime socket and files.getPublicUrl follow the same host. Nothing in the app sets it.
  • Otherwise the page-relative default ('', or /p/<org>.<app> on the shared edge): a local isomorph run and a page without the shim are unchanged.

A value that is not an http(s) origin — a path, a trailing slash, another scheme, a non-string — is ignored. An explicit baseUrl always wins.

A refused call (401) is renewed once and resent once. On the page's own origin the renewal is the identity GET, which the platform edge heals. On a separate API host (the global above, when it is not the page origin) it is a credentialed GET <api>/__harbour/context/refresh?return_to=%2F_harbour%2Fidentity%2Fcurrent: the host mints a fresh identity cookie from its sign-in session and redirects to the identity GET. When that refresh does not end in a 200 with the user — the session is gone, the network failed — the original 401 is surfaced without a resend, and the page's sign-in handling takes over.

On a separate API host the identity GET itself (identity.current(), and the background refresh every 60 s that drives identity.onChange) is renewed the same way: its refresh ends at the identity GET, so that answer is the identity and no second GET is sent. When the refresh fails, identity.current() answers null and listeners hear null, as before. On the page's own origin the identity GET is never renewed (it is the renewal). (1.5.3)

The signed-in user's groups

user.groups — the company groups your identity provider sent at sign-in; use for in-app roles. App access itself is enforced before your code runs.

It is on identity.current(), identity.onChange and an action's ctx.user, and it is always an array ([] when the identity provider sent none):

// actions/approve.ts
export default async function approve(input: { id: string }, ctx: { user: { groups: string[] } }) {
  if (!ctx.user.groups.includes('Finance')) throw new Error('Only Finance can approve.');
  // ...
}

(1.6.0)

Live progress from an action

An action reports steps with ctx.progress(step) (a string or a small JSON object); a page that passes onProgress gets each one as it happens, then the result or the error exactly as a plain invoke gives it. No table, no polling.

// actions/research.ts
export default async function research(input: { url: string }, ctx: { progress(step: string): void }) {
  ctx.progress(`Opened ${input.url}`);
  // ...
  return result;
}

// page
await isomorph.actions.invoke('research', input, { onProgress: (step) => setSteps((steps) => [...steps, step]) });

Without onProgress the call is unchanged and ctx.progress does nothing. At most 256 steps of up to 1 KiB each reach the page; further steps are dropped. The action still has the 300 s action budget. (Unreleased)