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/extensions-sdk

v0.1.260923-alpha.2

Published

Public frontend SDK for GEA extensions.

Readme

@gea-ai/extensions-sdk

Public frontend SDK for GEA extensions.

Start with the public GEA extension developer docs and extension quick start.

Install

npm install @gea-ai/extensions-sdk

Minimal Shape

import {
  document,
  defineExtension,
  field,
  table,
  wiki,
} from "@gea-ai/extensions-sdk";

export const extensionDeclaration = defineExtension({
  frontend: {
    entry: "src/index.tsx",
  },
  name: "Customer Research",
  resources: {
    workspace: wiki({
      children: {
        notes: table({
          fields: {
            note: field.text({
              indexed: true,
              required: true,
              title: "Note",
            }),
          },
          title: "Notes",
        }),
        summary: document({
          initialContent: {
            format: "markdown",
            markdown: "# Customer Research Summary\n",
          },
          title: "Summary",
        }),
      },
      title: "Customer Research",
      visibility: "workspace",
    }),
  },
  slug: "customer-research",
});
// src/extension-runtime.ts
import { createExtensionRuntime } from "@gea-ai/extensions-sdk/react";
import extensionDeclaration from "../gea.extension";

export const { mount, useExtensionApp, useLocale, useTranslations } =
  createExtensionRuntime({
    declaration: extensionDeclaration,
  });
// src/index.tsx
import { useExtensionApp } from "./extension-runtime";

export default function CustomerResearch() {
  const app = useExtensionApp();

  async function submitNote(note: string) {
    await app.resources.notes.records.create({
      note,
    });
  }

  return <button onClick={() => void submitNote("Follow up")}>Add</button>;
}

SQLite Database Authoring

database.sqlite declares a migration-backed Table without repeating its relation schema in gea.extension.ts:

import { database, defineExtension, wiki } from "@gea-ai/extensions-sdk";

export default defineExtension({
  frontend: { entry: "src/index.tsx" },
  name: "Customer Research",
  resources: {
    workspace: wiki({
      children: {
        appData: database.sqlite({
          migrations: "drizzle",
          scope: "workspace",
          title: "Application Data",
          visibility: "backend",
        }),
      },
      title: "Customer Research",
      visibility: "workspace",
    }),
  },
  slug: "customer-research",
});

The migration directory may contain ordered .sql files and Drizzle's meta/ directory. gea ext validate, gea ext package, and gea ext sync replace the directory with a checksummed migration inventory and include every SQL file in the signed extension component.

SQLite databases are additive within declaration version gea.extension/v1. Older CLIs still fail closed because their v1 resource parser rejects the unknown engine field instead of silently packaging it as a native Table.

The host provisions and upgrades this resource before extension activation. A visibility: "backend" database stays out of Context navigation; use visibility: "context" when workspace members and Agents should also browse or query it through the ordinary Context Table APIs. In both cases, extension code receives a typed Drizzle SQLite Proxy handle, never a SQLite path, storage locator, or credential:

import {
  defineBackendActions,
  type ExtensionBackendSdk,
} from "@gea-ai/extensions-sdk/backend";
import * as schema from "./schema";
import { actions } from "../contract";

type Sdk = ExtensionBackendSdk<never, { appData: typeof schema }>;

export const backend = defineBackendActions<typeof actions, Sdk>(actions, {
  createIdea: async ({ input, sdk }) =>
    await sdk.databases.appData.transaction(async (tx) => {
      const idea = { id: crypto.randomUUID(), title: input.title };
      await tx.insert(schema.ideas).values(idea);
      return idea;
    }),
});

Normal Drizzle selects, writes, batches, and interactive transactions execute against the host-owned database. The host revalidates the installed extension, workspace, declared resource key, engine readiness, and caller on every proxy operation. The same proxy protocol is used by gea ext dev.

Frontend code calls host APIs through SDK helpers. The SDK uses the GEA host postMessage bridge by default, so extension authors do not need to write message plumbing or know the host ORPC transport.

The frontend app.gea.chats surface starts and continues both legacy and SDK-first Agent chats. When a legacy Agent pauses for a request Tool result, submit only the persisted assistant message ID, exact runtime Tool call ID, and JSON output. The host validates the pending Tool call, persists its result, and queues the continuation Run in one transaction:

await app.gea.chats.submitToolOutput({
  chatId,
  messageId: pendingMessage.id,
  output: { tone: "concise" },
  tool: "request",
  toolCallId: pendingRequest.toolCallId,
});

When an SDK-first Agent pauses before a Tool call, an extension may instead respond with only the persisted assistant message ID and the approval decisions:

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

Runtime selection stays server-side: submitToolOutput rejects SDK-first Agent chats, while respondToToolApprovals rejects legacy Agent chats.

Actions And Backend Packages

Curated backend-capable extension packages can share one typed action contract between frontend and backend code. The frontend calls the host transport, while the backend exports handlers that the host loads through ORPC after install, membership, and runtime resource checks.

// src/contract.ts
import { defineActions } from "@gea-ai/extensions-sdk/actions";
import z from "zod";

export const actions = defineActions({
  ping: {
    inputSchema: z.object({ value: z.string() }),
    outputSchema: z.object({ echoed: z.string() }),
  },
});
// src/backend/index.ts
import { defineBackendActions } from "@gea-ai/extensions-sdk/backend";
import { actions } from "../contract";

export const backend = defineBackendActions(actions, {
  ping: async ({ input }) => ({ echoed: input.value }),
});
// src/frontend/index.tsx
import {
  createExtensionActionClient,
  type ExtensionActionTransport,
} from "@gea-ai/extensions-sdk/actions";
import { actions } from "../contract";

declare const callExtensionAction: ExtensionActionTransport;

const client = createExtensionActionClient({
  actions,
  extensionSlug: "example-extension",
  transport: callExtensionAction,
});

await client.call("ping", { value: "hello" });

Private organization extension packages and curated marketplace packages run hosted backend actions in an operating-system sandbox. The bundle is read-only, temporary storage and the private host RPC socket are the only writable/runtime paths, ambient environment variables are cleared, and network access is denied. fetch is unavailable inside the backend; external calls must use explicit SDK methods such as connectors and webSearch. Backend code also cannot start child processes or worker threads. Remote backend files are checked against manifest sha256 and sizeBytes metadata before execution, and the host dispatches only declaration-listed backend actions. The public backend SDK surface is intentionally narrow:

  • sdk.state.records.* for extension-private, authenticated-user-owned workflow state. Reads and mutations cannot cross principals in the same workspace.
  • sdk.connectors.execute(...) for host-managed connector calls.
  • sdk.webSearch(...) for provider-neutral public web search with host-owned credentials and usage metering.
  • sdk.artifacts.path(...), writeText(...), and writeJson(...) for extension artifacts.
  • sdk.resources.records.create/createWithArtifact/get/update(...) and sdk.resources.records.findByIndexedText(...) for idempotent lookup on a declared indexed text field, plus sdk.resources.documents.update(...) for declared GEA resources.
  • sdk.agents.call(slug, input) and generated agent handles for declared package-internal agents. Generated handles may receive ordered uploaded-file metadata in attachments.
  • sdk.chats.list(...) for listing the authenticated user's ordinary top-level chats in the current workspace, plus sdk.chats.messages.list(...) for reading a selected chat.
  • sdk.databases.<handle> for typed Drizzle SQLite Proxy queries, batches, and transactions against SQLite resources declared by this extension. No path, provider locator, credential, or arbitrary resource lookup is exposed.

Legacy host helpers outside this surface are not part of the third-party SDK contract.

Nested-agent calls may include runtimeReminders: string[]. The host passes these reminders unchanged into the agent runtime, where they are assembled as system reminders outside the structured user input. Extension backends should derive reminder content from server-resolved action context such as context.extension.appResourceId and context.extension.resources; frontend input must not be trusted to identify extension-owned resources.

resources.records.findByIndexedText(...) accepts only a field declared by the current extension table as type: "text" and indexed: true. The lookup stays inside the current organization, workspace, declared table, and optional scope resource. It returns null for no match and rejects duplicate matches instead of selecting one silently.

resources.records.get(...) reads one row by its stable table record ID. The host enforces the current user's resource permissions and rejects a record that does not belong to the declared extension table.

resources.records.createWithArtifact(...) imports one file produced by the current extension's declared agent into a declared Context Table file field:

const asset = await sdk.resources.records.createWithArtifact({
  agentRunId,
  artifactPath: "assets/logo.svg",
  chatId,
  data: {
    assetKey: `${generationId}:primary-logo`,
    generationId,
  },
  fieldKey: "file",
  resourceKey: "brandAssets",
});

The host anchors the relative path under the current extension's chat output directory, verifies the chat and declared-agent run, and creates the attachment value itself. Persist asset.id, the declared resource key, and the field key; do not persist a filesystem path, object-store key, or signed URL. Frontend table handles expose records.get(recordId) for resolving that stable reference through the same declared table boundary.

state.records.update({ data }) merges the provided object into the existing record data. Use this for partial progress updates; model full replacement as a future explicit API if an extension needs it.

Backend actions execute external systems through host-managed connectors:

const result = await sdk.connectors.execute({
  connector: "bocha-search",
  params: {
    method: "GET",
    operation: "request",
    path: "/search",
    query: { q: "consumer trend" },
  },
});

Connector credentials are resolved by normal GEA connector credential scope rules. Extensions should declare every connector dependency in connectorRequirements so install and setup flows can surface missing connectors or credentials before backend actions run. Provider-specific request details should live in API, MCP, or native connector implementations; backend actions should call those connectors instead of embedding provider credentials or direct network integration code.

Public web search is a first-class SDK capability rather than a connector:

const results = await sdk.webSearch({
  maxResults: 5,
  query: "site:example.com architecture intelligence",
});

The host chooses the provider and returns { title, url, content } results. Extensions do not declare a connector requirement or manage a search-provider credential for this capability.

A builtin_connector requirement activates the host-owned connector from the published extension package. It does not add a connector export or create a standalone connector app resource. The host still requires the connector to be enabled in the deployment environment and resolves any system or user credentials at execution time.

export const extensionDeclaration = defineExtension({
  // ...
  connectorRequirements: [
    {
      slug: "bocha-search",
      source: { type: "app_resource", slug: "bocha-search" },
    },
    {
      slug: "wechat-official-account",
      source: {
        type: "builtin_connector",
        slug: "wechat-official-account",
      },
    },
  ],
});

Installed extension summaries expose each declared connector as connected, missing_connector, or missing_credentials. Backend action dispatch preflights the same setup state before creating the connector runtime and fails with connector_setup_required when setup is incomplete.

Host Account And Usage

Hosted Extension frontends can read the current public user profile, query that user's token usage in the active workspace, let authorized organization administrators query organization-wide usage, and invoke password-account actions without importing the host auth or ORPC clients:

const user = await app.account.getCurrentUser();
const usage = await app.usage.getSummary({
  from: "2026-08-01T00:00:00.000Z",
  grain: "day",
  to: "2026-09-01T00:00:00.000Z",
});
const organizationUsage = await app.usage.getOrganizationSummary({
  from: "2026-08-01T00:00:00.000Z",
  grain: "day",
  to: "2026-09-01T00:00:00.000Z",
  workspaceId: "workspace-id",
});

await app.account.changePassword({
  currentPassword,
  newPassword,
  revokeOtherSessions: true,
});

await app.account.signOut();

getCurrentUser() returns only id, email, name, and image. Both usage methods accept time, grain, model, request-type, Agent, and Project filters. For getSummary, the host injects the authenticated user and active workspace. For getOrganizationSummary, an authorized administrator may additionally filter by workspace, while the host keeps organization identity from the authenticated session. The existing usage service requires the view_enterprise_usage permission and returns the same organization totals plus user and workspace breakdowns used by the administration UI. Neither method accepts user or organization identity. Password values are passed without trimming. These account operations require a hosted browser session; the loopback local-development helper does not own a browser session to read the profile, change a password, or sign out. Token usage remains available in local development under the authenticated CLI user and ordinary organization usage authorization.

Host File Uploads

Extension frontends prepare a tenant-scoped upload through the host, then send the bytes directly to object storage. File bytes do not pass through postMessage:

const prepared = await app.files.prepareUpload({
  contentType: file.type || "application/octet-stream",
  name: file.name,
  sizeBytes: file.size,
});

const response = await fetch(prepared.upload.url, {
  body: file,
  credentials: "omit",
  headers: prepared.upload.headers,
  method: prepared.upload.method,
});

if (!response.ok) {
  throw new Error(`Upload failed with status ${response.status}.`);
}

One file is limited to 50 MiB, and its exact name is limited to 240 UTF-8 bytes. A declared-agent run accepts at most 20 files and 100 MiB in total. The extension keeps the original name, contentType, and sizeBytes together with prepared.sourceUrl and prepared.attachmentToken in its own private state when the source must survive a reload. The attachment token expires after 24 hours, so a later run must ask the user to upload the source again.

A backend action passes those values to a declared package agent without constructing Lexical content or a workspace path:

await sdk.agents.brandDnaGenerator.run({
  attachments: sourceFiles.map((file) => ({
    attachmentToken: file.attachmentToken,
    contentType: file.contentType,
    name: file.name,
    sizeBytes: file.sizeBytes,
    sourceUrl: file.sourceUrl,
  })),
  chatId,
  message: "Analyze the uploaded official sources.",
});

The host assigns each attachment an identifier, derives /workspace/uploads/<extensionSlug>/<attachmentId>/<filename>, and uses the existing agent upload pipeline to materialize each source before the run. Before materialization, the runtime verifies that the signed capability matches the current user, workspace, installed extension, source URL, and exact metadata. The token is not included in model input or rendered chat messages.

Host Skills, Themes, And Chat Streaming

useExtensionApp() also exposes small, permission-checked host APIs for creating a user skill, installing an organization custom theme, and reading the authenticated user's workspace chats:

const app = useExtensionApp();

await app.skills.create({
  // Use a stable operation identifier when a retry must return the same skill.
  idempotencyKey: generationId,
  files: [{ path: "SKILL.md", content: generatedSkillMarkdown }],
});

await app.appearance.installCustomTheme({
  ...generatedTheme,
  apply: true,
  logo: {
    fieldKey: "file",
    recordId: logoAssetRecordId,
    resourceKey: "brandAssets",
  },
});

const chats = await app.chats.list({ limit: 30 });
const selectedChat = chats.items[0];
const summary = await app.chats.get({ chatId: selectedChat.id });
const page = await app.chats.messages.list({
  chatId: selectedChat.id,
  limit: 50,
});

app.chats.list(...) returns only the current user's non-project, top-level, non-deleted chats in the current workspace. Continue pagination with its opaque nextCursor; do not inspect or persist assumptions about the cursor format. The returned item contains the chat title, update and pin timestamps, latest Agent identity, and isRunning. Pass the selected id to get, messages.list, and createStreamAccess.

An organization administrator with the existing export_organization_chat_traces permission can inspect user-owned chats across active workspaces without receiving per-chat access:

const organizationChats = await app.chats.admin.list({
  limit: 30,
  workspaceId,
  // userId is also optional.
});
const transcript = await app.chats.admin.messages.list({
  chatId: organizationChats.items[0].id,
  limit: 100,
});

The administrator list includes owner and workspace identity. Its optional cursor is returned by the host and must be passed back unchanged. Message pages use the same raw chronological trace representation and audit behavior as the organization chat export API. Both methods recheck the signed-in member and the current organization permission. Owner has this permission by default; System Admin and custom roles require an explicit grant. No deployment-level system configuration or target Workspace membership is required.

Authorized administrators can use the existing role-management surface from an Extension without importing ORPC directly:

const state = await app.permissions.listRoleManagementState();
await app.permissions.roles.permissions.update({
  roleId: systemAdminRoleId,
  permissions: [...currentPermissions, "export_organization_chat_traces"],
});
await app.permissions.setMemberManualRoles({
  userId,
  roleIds: [analystRoleId],
});

app.permissions mirrors the existing permission-checked Role, Member Role, and Workspace Admin procedures. It does not add privileges: every read and mutation is authorized and audited by the same server procedures used by the administration UI.

app.skills.create({ idempotencyKey }) accepts an optional stable operation key. The host combines it with the authenticated tenant, principal, installed extension, and canonical source files. Retrying the same operation after a reload or lost response returns the same installed skill; changing the files produces a distinct operation.

Theme logo inputs are stable references to exact rows in declared Context Tables. The host enforces the current user's table and record-scope view permissions, validates the referenced image bytes, constrains the logo to a 2048-pixel maximum dimension, and copies it into organization-owned appearance storage. Extracted fonts remain extension content for brand guidance and generated skills; custom themes do not install font files into the host application.

These request-response calls use the SDK bridge. Live tokens do not pass through postMessage. Ask the host for a short-lived stream capability and give it directly to the AI SDK transport:

import { useChat } from "@ai-sdk/react";
import { EXTENSION_CHAT_STREAM_PROTOCOL } from "@gea-ai/extensions-sdk";
import { DefaultChatTransport } from "ai";
import { useMemo } from "react";
import { useExtensionApp } from "./extension-runtime";

function Transcript({ chatId }: { chatId: string }) {
  const app = useExtensionApp();
  const transport = useMemo(
    () =>
      new DefaultChatTransport({
        api: "/api/chat",
        prepareReconnectToStreamRequest: async ({ id }) => {
          const access = await app.chats.createStreamAccess({ chatId: id });

          if (access.protocol !== EXTENSION_CHAT_STREAM_PROTOCOL) {
            throw new Error(`Unsupported stream protocol: ${access.protocol}`);
          }

          return {
            api: access.url,
            credentials: "omit",
            headers: { Authorization: `Bearer ${access.token}` },
          };
        },
      }),
    [app],
  );
  const chat = useChat({ id: chatId, resume: false, transport });

  return <button onClick={() => void chat.resumeStream()}>Open stream</button>;
}

The capability is bound to the current user, organization, workspace, installed extension, chat, and exact latest Agent identity. The chat may come from a declared nested Agent or a top-level Agent task started through the user-scoped SDK. It expires after five minutes. The direct request omits cookies and uses the AI SDK UI-message stream protocol, so extension code keeps the normal useChat experience while the host retains authorization and identity ownership.

Extensions should render those messages with the public protocol renderer instead of rebuilding individual AI SDK parts:

npm install @gea-ai/chat-ui
import {
  type AgentChatMessage,
  type GeaAddToolOutputInput,
  GeaChatUIProvider,
  GeaMessages,
} from "@gea-ai/chat-ui";
import "@gea-ai/chat-ui/styles.css";

const chat = useChat<AgentChatMessage>({
  id: chatId,
  resume: false,
  transport,
});

async function submitToolOutput(input: GeaAddToolOutputInput) {
  if (!input.messageId) {
    throw new Error("A persisted assistant message ID is required.");
  }

  await app.gea.chats.submitToolOutput({
    chatId,
    messageId: input.messageId,
    output: input.output,
    tool: "request",
    toolCallId: input.toolCallId,
  });

  const { messageId: _messageId, ...localToolOutput } = input;
  await chat.addToolOutput(localToolOutput);
  await chat.resumeStream();
}

return (
  <GeaChatUIProvider
    actions={{ addToolOutput: submitToolOutput }}
    theme={{ accent: "#d85f5f", radius: "16px" }}
  >
    <GeaMessages messages={chat.messages} status={chat.status} />
  </GeaChatUIProvider>
);

The package handles standard message parts and yaml-render, but it does not own Chat transport or authorization. Extensions may override semantic theme variables, stable data-slot selectors, and visual component slots without changing message or request submission semantics.

In the useChat configuration above, AI SDK chat.addToolOutput only updates the local message. Configuring AI SDK sendAutomaticallyWhen can make that method invoke its own Chat transport, but it still does not call GEA's canonical Tool-output continuation API. Call the host-owned app.gea.chats.submitToolOutput first, then update local state and resume the newly created stream as shown above. If this action is omitted, display-only YAML continues to render and interactive request controls are not mounted.

Interactive YAML is read directly from tool.input.yaml without a Markdown fence and rendered at that request tool part's position. Submission targets the same part's runtime toolCallId and sends the complete current form state. Legacy request-key parts and interactive YAML copied into assistant text are not mounted. During input-streaming, partial YAML is display-only: the renderer shows a loading placeholder or a locked last-valid preview and never calls the host. input-available triggers final strict validation and unlocks the form; the existing persisted messageId plus toolCallId submission path is unchanged.

Host Context, Theme, And Locale

Packaged and local-dev hosts install the parent message bridge before activating the extension iframe, then send a host-context message on load and when the host document theme or locale attributes change. Calls made during the extension's first mount therefore reach the host even when the extension declares no resources. React extensions should normally use createExtensionRuntime from @gea-ai/extensions-sdk/react; it wires host-context sync into ExtensionRuntimeProvider and exposes:

  • useExtensionApp() for the typed host SDK client and resolved resource handles.
  • useLocale() for the current resolved locale.
  • useTranslations(namespace) for extension-owned local dictionaries.
  • mount(Component) for rendering the default no-props React component into #root.

The context includes locale, text direction, resolved color scheme, resolved theme, and CSS custom properties such as --background, --foreground, and --border. Hosted web context also includes navigation.href, the absolute host route URL, and navigation.search, the current route search values. Use app.navigation.updateSearch(...) to change extension-owned route state and app.navigation.openExternal({ url }) to open an HTTP(S) URL in a host-owned browser tab. Sandboxed frontend code must not read window.parent.location or use popup permissions. The CLI scaffold wires this into src/extension-runtime.ts, includes a small local i18n dictionary, and configures Tailwind CSS so utilities like bg-background, text-foreground, and border-border follow the host theme. Extension messages remain extension-owned; the host only supplies the active locale.

Upload And Publish

Use gea ext sync before the first local session and after changing the declaration, agents, or skills. Sync uploads an owner-private runtime draft and provisions or reuses its declared workspace resources; it does not build or upload frontend/backend code. Use gea ext package for local package inspection, then gea ext deploy to upload the complete extension package as an app-resource draft. Deployment does not publish. Review and publish that app resource from the GEA web UI, using the same lifecycle as other app resources.

The generated artifact is a canonical gea.resource_package.v1 directory. Its root manifest.json exports the extension, while private agents and skills stay under agents/<slug> and skills/<slug> as non-exported components. The extension frontend and backend stay under extensions/<slug>. Deployment uploads this complete generated directory; source repositories, tests, and node_modules are not part of the runtime package. The CLI bundles backend runtime dependencies into the generated backend entrypoint, so hosted execution does not depend on packages installed in the GEA server image.

The app-resource publish version is the canonical hosted version. gea.extension.ts version metadata is stored in the normalized manifest as package metadata. Published extension frontend assets are served by the host through a sandboxed iframe asset route. Hosted frontends run with sandbox="allow-scripts allow-downloads allow-forms" and the matching CSP sandbox directive so React and other client-side form handlers can receive submit events. The CSP keeps form-action 'none', so native form navigation and submission remain blocked. Declared nested HTTPS frames retain a network-scheme ancestor. Host control calls use the postMessage bridge; live chat bytes use the scoped, cookie-free direct AI SDK stream described above.

Set Vite base to "./" and import Extension-owned images, fonts, and media from frontend source whenever possible. A file copied beside the generated frontend entrypoint may instead be referenced as ./file-name; never use a root-absolute path such as /logo.png, because that resolves against the GEA tenant site rather than the installed package.

Resource Handles

Extensions declare durable resources in defineExtension. Accepting an owner-private runtime draft provisions or reuses real GEA resources and binds them to the extension scope. gea ext dev verifies that synchronized draft and its bindings only when the declaration uses package-internal agents, skills, connector requirements, or declared resources. A stateless frontend declaration skips Extension resource and owner authorization; its hosted API calls continue to authenticate the current workspace member and authorize each target resource normally. The command then prints a capability-bearing localhost URL that can be opened directly without registration inside GEA. That bootstrap URL transfers an in-memory loopback capability and replaces itself with the Vite frontend as the top-level page. The SDK reads lifecycle.stage = "ready" plus any resolved resource handles from that bootstrap and exposes them as typed app.resources when the frontend uses useExtensionApp() from a runtime created with the declaration.

Declared resource handles are an authoring and lifecycle convenience, not an additional permission boundary. The curated app.gea.context methods may also operate on existing Context resources. Every call runs as the current signed-in user and keeps the target Context route's ordinary API-scope, organization, workspace, resource, and row-scope authorization; an Extension never gains permissions that the user does not already have.

Use the engine-neutral table data APIs for existing or uploaded tables. Always describe a table when the engine or relation is not already known, then browse the default relation or pass an advertised relation ID:

const descriptor = await app.gea.context.tableData.describe({ tableId });
const page = await app.gea.context.tableData.browse({
  tableId,
  limit: 50,
  offset: 0,
});

tableData.browse works for native and managed table engines. The legacy context.records methods remain the editable-record API for native Context Tables and must not be used to read an uploaded DuckDB or SQLite table.

Warning: Syncing a declaration that removes resources may deactivate their bindings immediately, including bindings used by the currently published extension. The underlying workspace resources and their data are not deleted. Review resource removals before running gea ext sync.

Use those handles for extension-owned durable data:

await app.resources.notes.records.create({
  note: "Follow up with the design team",
});

A complete research-queue extension can declare a workspace-visible wiki, store submitted URLs in a declared context table, place an aggregate summary document next to that table, and start a hosted Agent task with only the resolved table and document locations in the prompt.

Third-party extensions should not persist private UI state through production extension_record. Use browser-local or dev-runtime-local state for UI-only state. Durable user-visible data should be modeled as declared GEA resources so users and admins can inspect, recover, export, permission, and delete it through normal resource flows.

Current table APIs are not yet optimized for large database workloads. Treat 10k to 100k row scenarios, server-side filtering/sorting, relation joins, and benchmarks as a separate table performance track.

The root SDK avoids private monorepo imports so extension projects can type-check and build outside the GEA monorepo. Public context resource result types are exported from @gea-ai/extensions-sdk/context. The portable schemas and TypeScript types are owned by @gea-ai/contract and re-exported by the SDK.

Local-dev extensions are trusted developer code. They do not declare manifest permissions yet. The host bridge exposes only curated SDK methods, but it does not enforce extension-specific grants. SDK calls run as the current signed-in user and remain constrained by normal hosted ORPC user, workspace, and resource permissions.