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

@gea-ai/app-sdk

v0.1.260923-alpha.1

Published

Server-side helpers for apps that use GEA OAuth or an organization API key.

Readme

@gea-ai/app-sdk

Server-side helpers for apps that use GEA OAuth or an organization API key.

Each app package has one client_id and one client secret. Installing it creates an organization-owned app_resource; its id is the immutable installation ID.

Keep the client secret, authorization codes, refresh tokens, and access tokens in the app backend. Browser code should only receive the app's own session.

OAuth client

import { createOAuthClient } from "@gea-ai/app-sdk";

const gea = createOAuthClient({
  baseUrl: process.env.GEA_BASE_URL!,
  clientId: process.env.GEA_CLIENT_ID!,
  clientSecret: process.env.GEA_CLIENT_SECRET!,
});

The SDK requires HTTPS because OAuth client credentials and bearer tokens are sent to this origin. Plain HTTP is accepted only for local development hosts such as musegea.localhost, its tenant subdomains, localhost, and loopback.

User authorization

When a user opens an installed app from GEA, the launch URL includes the public client_id, installation_id, and organization_id. Use the organization ID as an untrusted selector, create a random state and PKCE verifier in the app backend, then redirect the browser to GEA:

const authorizationUrl = gea.getAuthorizationUrl({
  codeChallenge,
  organizationId,
  redirectUri: "https://app.example.com/oauth/callback",
  scope: ["openid", "profile", "offline_access", "resources:read"],
  state,
});

GEA validates the selector against the OAuth client, active installation, and signed-in user's active membership. The selector does not grant access by itself. Direct authorization without an organization selector is not supported in this version.

After validating state in the callback, exchange the one-time code:

const token = await gea.exchangeAuthorizationCode({
  code,
  codeVerifier,
  redirectUri: "https://app.example.com/oauth/callback",
});

The response includes verified organization and installation metadata. It also includes a refresh token when offline_access was granted:

const renewed = await gea.refreshUserToken({
  refreshToken: token.refreshToken!,
});

Refresh tokens rotate. Persist the replacement returned by every successful refresh and discard the previous value.

Revoke an access or refresh token when the app session ends or credentials are otherwise no longer needed:

await gea.revokeToken({
  token: token.accessToken,
  tokenTypeHint: "access_token",
});

Tenant authorization

Use a tenant token only for app-level APIs that do not act as a user. An organization-local app omits an installation selector because GEA infers its owning organization:

const tenantToken = await gea.getTenantToken({
  scope: ["organization:read", "resources:read"],
});

A Marketplace app supplies the immutable installation ID:

const tenantToken = await gea.getTenantToken({
  installationId: "019...",
  scope: ["organization:read", "resources:read"],
});

An organization slug is never accepted as the authorization boundary. Tenant tokens do not contain a user principal and cannot call user, membership, or workspace-member APIs.

ORPC client

Create an API client with either a user or tenant access token:

import { createClient } from "@gea-ai/app-sdk";

const api = createClient({
  accessToken: token.accessToken,
  baseUrl: process.env.GEA_BASE_URL!,
});

Only routes that explicitly accept the token kind and scopes are available. GEA revalidates the OAuth client, package, installation, organization, membership for user tokens, and requested scopes on every request.

resources.list() returns active organization-owned resources shared with all workspaces or at least one specific workspace. User-owned and user-only resources are excluded.

Agent chats

Agent chat APIs require a user access token with agents:read, agent_chats:read, and/or agent_chats:write. Tenant tokens cannot use these scopes because every chat runs as the authorizing user in one explicit workspace.

Create a workspace client with the organization slug from the token response and a workspace slug selected or configured by your app:

import { createWorkspaceClient } from "@gea-ai/app-sdk";

const gea = createWorkspaceClient({
  accessToken: token.accessToken,
  baseUrl: process.env.GEA_BASE_URL!,
  organizationSlug: token.organization.slug,
  workspaceSlug: appWorkspaceSlug,
});

const agents = await gea.agents.list();
const run = await gea.agentChats.start({
  agentId: agents[0]!.id,
  message: "Summarize the latest project context.",
});

const { chatId } = run;
const stream = await gea.agentChats.openStream({ chatId });
// Consume stream.body as an AI SDK UI message stream.

await gea.agentChats.sendMessage({
  chatId,
  message: "Turn that into three action items.",
});

const page = await gea.agentChats.messages.list({ chatId, limit: 50 });

When an SDK-first Agent pauses on Tool approval, read the pending assistant message and submit decisions without echoing Tool input:

await gea.agentChats.respondToToolApprovals({
  chatId,
  messageId: pendingMessage.id,
  responses: [{ id: pendingApproval.id, approved: true }],
});

The same Chat methods also work with legacy Agents. Runtime selection is owned by the selected Agent's server-side metadata; Tool approval responses are accepted only for SDK-first Agent Worker chats.

Agent APIs reject organization-only RPC routes. The workspace slug must be present in the request route; GEA never selects a fallback workspace for a third-party app.

GEA generates persisted chat and message IDs. Read chatId from the result of agentChats.start() and use it for later stream, message, and history calls. start() and sendMessage() do not currently provide idempotent retry semantics, so do not automatically retry either operation after an ambiguous network failure. With the corresponding scopes, the app can read or continue any root chat owned by the authorizing user in the selected organization and workspace; the chat does not have to originate from that app. Chats created through this API record the source installation in chat.metadata for provenance only; it is not an authorization boundary.

openStream() exchanges the user token server-side for a short-lived, chat-bound stream capability, then opens the public streaming endpoint with that capability. The capability remains bound to that exact OAuth grant: GEA rechecks the grant, its read scope, and the user's current workspace access when the stream opens. Revoking the grant or removing workspace access makes an already-issued capability unusable. Keep both the user token and stream response in the app backend; relay only the data your own frontend needs. GEA briefly waits for a new chat run to register its stream. If the response is 503 with Retry-After, call openStream() again with the same chatId. A 204 response means the run finished before a stream consumer attached; call agentChats.messages.list() with the same chatId to read the final messages.

The stream capability contains only opaque grant, installation, and chat references. GEA resolves the user, organization, installation, and workspace server-side when the stream opens. Message pages preserve Agent sender metadata but redact the GEA user's internal ID and name. Use the OAuth pairwise sub and the explicitly granted profile claims when your app needs user identity.

API-key Agent chats

Use createApiKeyWorkspaceClient for the API-key-only Agent surface. A user key omits externalUserId and sends only its Bearer credential:

import { createApiKeyWorkspaceClient } from "@gea-ai/app-sdk";

const gea = createApiKeyWorkspaceClient({
  apiKey: process.env.GEA_API_KEY!,
  baseUrl: process.env.GEA_BASE_URL!,
  organizationSlug: "acme",
  workspaceSlug: "research",
});

const chatId = crypto.randomUUID();
const stream = await gea.agentChats.run({
  agentId: "agent-019...",
  chatId,
  message: "Summarize this workspace.",
});

if (!stream.ok || !stream.body) {
  throw new Error(`Agent Chat failed: ${stream.status}`);
}

A team key must create a client for one directory identity. Set externalUserId to the exact case-sensitive directory principalId / externalId; the SDK sends it as x-user-id on every RPC request:

const gea = createApiKeyWorkspaceClient({
  apiKey: process.env.GEA_TEAM_API_KEY!,
  baseUrl: process.env.GEA_BASE_URL!,
  externalUserId: customerDirectoryPrincipalId,
  organizationSlug: "acme",
  workspaceSlug: "research",
});

The deployment must enable features.teamApiKeyIdentityDelegation. Team keys are restricted to this Agent API and its three Agent scopes; hosted MCP and general scoped ORPC routes accept personal keys only.

The client exposes agents.list(), agentChats.run(), agentChats.openStream(), and agentChats.messages.list(). run() sends one POST /api/chat request and returns its AI SDK UI-message stream directly. Store both kinds of API key on the server. Do not relay a team key or let a browser choose x-user-id independently of the authenticated customer user.

API-key Agent access uses the independent agents.read, agent_chats.read, and agent_chats.write scopes; OAuth's colon-delimited scopes remain unchanged. run() requires agent_chats.write; openStream() and persisted message reads require agent_chats.read. Both direct HTTP requests send the same API key and, for a team key, the same fixed x-user-id. GEA resolves that identity and rechecks the Key, user, Chat ownership, and workspace access on every request.

Do not automatically retry run() after an ambiguous result. A 503 stream response may be retried by calling openStream() with the existing chatId; after 204, read the final persisted messages with agentChats.messages.list().

To attach files, call files.upload({ file, filename }). It sends one multipart POST /api/files with the same credentials, requires agent_chats.write, and returns the raw Response; a 201 body contains the file id. Pass those ids as fileIds to run() together with a string message:

const uploaded = await gea.files.upload({
  file: pdfBlob,
  filename: "report.pdf",
});
if (uploaded.status !== 201) {
  throw new Error(`Upload failed: ${uploaded.status}`);
}
const { id } = await uploaded.json();

const stream = await gea.agentChats.run({
  agentId: "agent-019...",
  chatId,
  fileIds: [id],
  message: "Analyze this report.",
});

Each file is at most 20 MiB and a run accepts at most 10 fileIds. A file can be used once, only by the user who uploaded it, in the same workspace; an unavailable id makes run() return 404 without starting a run.

AI SDK browser integration

The canonical step-by-step tutorial is Stream Agent responses in the GEA developer docs. The same core pattern is included here for SDK readers.

GEA returns an AI SDK UI message stream, but the GEA user access token and the short-lived stream capability must stay on the app server. Expose an authenticated route in your own app that opens the GEA stream and relays its body and protocol headers to the browser:

// app/api/gea-agent-chat/route.ts
import { createWorkspaceClient } from "@gea-ai/app-sdk";

const streamHeaders = [
  "cache-control",
  "content-type",
  "retry-after",
  "x-vercel-ai-ui-message-stream",
] as const;

async function getGeaForCurrentUser() {
  // Load and refresh the current user's OAuth token in your app backend.
  const connection = await loadUserGeaConnection();

  return createWorkspaceClient({
    getAccessToken: connection.getAccessToken,
    baseUrl: "https://musegea.com",
    organizationSlug: connection.organizationSlug,
    workspaceSlug: connection.workspaceSlug,
  });
}

export async function POST(request: Request) {
  const { agentId, message } = await request.json();
  const gea = await getGeaForCurrentUser();

  // Do not automatically retry this write after an ambiguous network failure.
  const run = await gea.agentChats.start({ agentId, message });
  return Response.json({ chatId: run.chatId });
}

export async function GET(request: Request) {
  const search = new URL(request.url).searchParams;
  const chatId = search.get("chatId");
  const view = search.get("view");
  if (!chatId || (view !== "messages" && view !== "stream")) {
    return new Response("chatId and view are required", { status: 400 });
  }

  const gea = await getGeaForCurrentUser();
  if (view === "messages") {
    return Response.json(
      await gea.agentChats.messages.list({ chatId, limit: 50 }),
    );
  }

  const upstream = await gea.agentChats.openStream({ chatId });
  const headers = new Headers();

  for (const name of streamHeaders) {
    const value = upstream.headers.get(name);
    if (value) headers.set(name, value);
  }

  return new Response(upstream.body, {
    headers,
    status: upstream.status,
    statusText: upstream.statusText,
  });
}

The browser can use the ai package to decode the relayed response. The transport subclass exposes AI SDK's normal response-stream parser; the browser does not need to understand the wire protocol itself:

"use client";

import { DefaultChatTransport, readUIMessageStream, type UIMessage } from "ai";
import { useState } from "react";

class GeaStreamTransport extends DefaultChatTransport<UIMessage> {
  decode(stream: ReadableStream<Uint8Array>) {
    return this.processResponseStream(stream);
  }
}

const transport = new GeaStreamTransport();

function textFromMessage(message: UIMessage) {
  return message.parts
    .filter((part) => part.type === "text")
    .map((part) => part.text)
    .join("");
}

async function openStream(chatId: string) {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(
      `/api/gea-agent-chat?view=stream&chatId=${encodeURIComponent(chatId)}`,
    );
    if (response.status !== 503) return response;

    const retryAfter = Number(response.headers.get("retry-after"));
    await response.arrayBuffer();
    await new Promise((resolve) =>
      window.setTimeout(
        resolve,
        Number.isFinite(retryAfter) && retryAfter > 0
          ? Math.min(retryAfter * 1000, 5000)
          : 500,
      ),
    );
  }

  throw new Error("The Agent stream was not ready after three attempts.");
}

export function AgentResult() {
  const [text, setText] = useState("");
  const [persistedMessages, setPersistedMessages] = useState<unknown>(null);

  async function run() {
    const startResponse = await fetch("/api/gea-agent-chat", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        agentId: "agent-019...",
        message: "Summarize this workspace.",
      }),
    });
    if (!startResponse.ok) throw new Error("Could not start Agent chat");
    const started = (await startResponse.json()) as { chatId: string };

    const response = await openStream(started.chatId);

    // 204 means the run completed before the stream attached. In that case,
    // skip the loop and read the persisted result below.
    if (response.status !== 204) {
      if (!response.ok || !response.body) throw new Error("Stream failed");

      const chunks = transport.decode(response.body);
      for await (const message of readUIMessageStream<UIMessage>({
        stream: chunks,
        terminateOnError: true,
      })) {
        setText(textFromMessage(message));
      }
    }

    const messagesResponse = await fetch(
      `/api/gea-agent-chat?view=messages&chatId=${encodeURIComponent(started.chatId)}`,
    );
    if (!messagesResponse.ok) throw new Error("Could not load final messages");
    setPersistedMessages(await messagesResponse.json());
  }

  return (
    <section>
      <button onClick={run} type="button">
        Run Agent
      </button>
      <pre>{text}</pre>
      <pre>{JSON.stringify(persistedMessages, null, 2)}</pre>
    </section>
  );
}

The persisted read after streaming is the source of truth when the run completes too quickly to attach a stream or when the browser reconnects. For a complete Next.js implementation with more detailed error handling, follow the public Stream Agent responses tutorial.