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

@palisadelabs/agent-sdk-react-native

v0.2.2

Published

Palisade in-app agent for React Native / Expo apps: assistant thread, nudges, home card, driver, outbox delivery. Headless core with theme + component injection; all copy, policy and capabilities are decided server-side.

Readme

@palisadelabs/agent-sdk-react-native

The Palisade in-app agent for React Native / Expo apps: the assistant thread (pushed route and global sheet), proactive nudge popups, support-reply delivery, the personalized home card, the agent driver (navigate / seed the pack builder / toast), and the agent-outbox claim. Ported verbatim from the CatchBack integration; everything host-specific arrives through one provider.

Design rules the package holds to:

  • Dumb pipes. Copy, policy, allowlists and deep links are decided server-side (via @palisadelabs/agent-relay in the host's Next app). New message kinds and capabilities never require an SDK release.
  • Custody. The SDK never holds client credentials. Session auth, storage, navigation, analytics and the pack builder are host callbacks.
  • Version skew. Unknown driver action types, unknown deep-link paths, unknown outbox kinds and unresolved destinations all no-op silently. The compiled screen deny set ships inside the SDK and no remote config or provider prop can override it.

Install

The package ships compiled JS (JSX preserved, Metro transpiles it) plus .d.ts in lib/.

npm install @palisadelabs/agent-sdk-react-native

Peer dependencies (all optional in package.json so a non-RN workspace install does not drag them in; the host app already has every one of these): react, react-native, @react-navigation/native, react-native-safe-area-context, react-native-reanimated, expo-haptics, expo-crypto, @expo/vector-icons.

Glue 1: the provider mount

Mount <PalisadeAgent> once, inside the NavigationContainer, wrapping the navigator (the same site where the reference app mounted its assistant watchers). It writes the environment seam and renders the three global hosts (nudge watcher, driver host, assistant modal host) after {children}.

import {
  PalisadeAgent,
  type AgentStorage,
} from '@palisadelabs/agent-sdk-react-native';
import { navigationRef } from './navigation/ref';
import { supabase } from './lib/supabase';
import { diskCache } from './lib/diskCache';
import { getBuilderState, getLastBuiltPack, clearLastBuiltPack } from './lib/builderState';
import { getRenderImageUrl } from './lib/images';
import { trackEvent } from './lib/catchbackPixel';
import MessageBubble from './components/messages/MessageBubble';
import GlassButton from './components/GlassButton';
import DragDismissSheet from './components/DragDismissSheet';

// Storage adapter over the existing disk cache. The SDK builds the SAME key
// strings the app used before (catchback_assistant_thread_v1_<userId>, etc.,
// via keyPrefix 'catchback'), so existing users' threads, A/B assignments and
// nudge cooldowns survive the migration byte for byte.
const agentStorage: AgentStorage = {
  getItem: (key) => diskCache.getString(key),
  setItem: (key, value) => diskCache.setString(key, value),
  removeItem: (key) => diskCache.remove(key),
};

<PalisadeAgent
  apiBaseUrl="https://catchback.app"
  enabled={process.env.EXPO_PUBLIC_ASSISTANT_ENABLED !== 'false'}
  getSession={async () => {
    const { data } = await supabase.auth.getSession();
    const s = data.session;
    return s ? { userId: s.user.id, accessToken: s.access_token } : null;
  }}
  storage={agentStorage}
  keyPrefix="catchback"
  navigationRef={navigationRef}
  assistantScreenName="AssistantChat"
  onEvent={(name, props) => trackEvent(name, props)}
  resolveImageUrl={(url, opts) => getRenderImageUrl(url, opts)}
  resolveDestination={(destination, params) => {
    // The driver's compiled semantic destinations. Only the host knows which
    // routes they map to. Return true = handled; false = the SDK falls back
    // (or no-ops, for the compiled trio). See "Destinations" below.
    if (!navigationRef.isReady()) return false;
    switch (destination) {
      case 'card':
        if (!params.card_id) return false;
        navigationRef.navigate('CardDetail', { cardId: String(params.card_id) });
        return true;
      case 'packs':
        navigationRef.navigate('MainTabs', { screen: 'Packs' });
        return true;
      case 'market':
        navigationRef.navigate('MainTabs', {
          screen: 'Market',
          params: params.query ? { query: String(params.query) } : undefined,
        });
        return true;
      default:
        return false; // unknown destinations fall through to the SDK's fallbacks
    }
  }}
  onSeedCustomPack={(seed) => {
    // The driver solved a custom pack; open the builder seeded with it (the
    // reference app's agentPack param shape). Return true = handled.
    navigationRef.navigate('MainTabs', {
      screen: 'Packs',
      params: { openCustom: true, agentPack: seed },
    });
    return true;
  }}
  builderContext={{
    // Read seam onto the host's pack-builder state; chat sends attach these
    // to page_context so the agent sees the pack on screen.
    getBuilderState,
    getLastBuiltPack,
    clearLastBuiltPack,
  }}
  theme={catchbackAgentTheme /* glass fills, Inter/Laira faces, brand colors */}
  components={{
    MessageBubble,        // the shared DM bubble; props are a superset of AgentMessageBubbleProps
    Button: GlassButton,  // props are a superset of AgentButtonProps
    Sheet: DragDismissSheet, // buys back finger-drag dismiss on the nudge sheet
  }}
>
  <RootNavigator />
</PalisadeAgent>

Notes:

  • enabled is the master build-time kill switch, the SDK equivalent of the reference app's EXPO_PUBLIC_ASSISTANT_ENABLED !== 'false' gate. False (or omitted) mounts nothing assistant-shaped; every exported lib function no-ops. Every feature on top stays behind the server's remote flags.
  • theme and components are partial: pass only what you override, neutral defaults fill the rest. A bare <PalisadeAgent> renders a clean generic chat with no host components at all.
  • components.Sheet is optional. Without it the SDK uses its internal bottom sheet (same motion, no finger-drag dismiss). The injected component receives AgentSheetProps (exported), which is a compatible subset of the CatchBack DragDismissSheet props, so the real component drops straight in.
  • resolveImageUrl(url, { width }) is the host's CDN image resizer (for CatchBack, lib/images getRenderImageUrl). The SDK calls it at every product/pack-art render and prefetch site (reply card thumbnails at width: 300, the home card's pack fan) so thumbnails never download full-size originals. Omitted, images load at their raw URLs — correct, but a real bandwidth and scroll-perf hit: wire it whenever the host has a resizing CDN.

Glue 2: modal provider mounts

AssistantModalProvider holds the shared open/close state behind every Ask button. <PalisadeAgent> already provides one as a fallback, and the provider is nest-safe (an inner one defers to an outer one). Mount your own in two places, exactly like the reference app:

  1. The App root, ABOVE <PalisadeAgent>, so screens outside the navigator subtree share the same modal state.
  2. Any standalone native surface tree that renders its own React root (for CatchBack: the rip-screen overlay tree). Each separate React root needs its own provider or useAssistantModal throws.
import { AssistantModalProvider, useAssistantModal } from '@palisadelabs/agent-sdk-react-native';

// any Ask button, in any header:
const { openModal } = useAssistantModal();

Glue 3: screen registration

Register the pushed thread route under the name AssistantChat (the deep-link fallback and the nudge sheet's support-reply tap navigate to this name; a different name also works if you pass it as assistantScreenName):

import { AssistantChatScreen } from '@palisadelabs/agent-sdk-react-native';

<Stack.Screen
  name="AssistantChat"
  component={AssistantChatScreen}
  options={{ presentation: 'card', animation: 'slide_from_right' }}
/>

Also register the Ask button's SHEET surface (same thread, sheet-presented; openModal() navigates here). Deliberately a navigator screen and not a React Native <Modal>: Fabric can orphan a Modal's host view as an invisible full-screen touch interceptor when its dismissal races the driver's navigation, freezing every tap in the app (root-caused live). The navigator's native sheet has no such failure mode. A different name works if you pass it as assistantSheetScreenName:

import { AssistantSheetScreen } from '@palisadelabs/agent-sdk-react-native';

<Stack.Screen
  name="AssistantChatSheet"
  component={AssistantSheetScreen}
  options={{ presentation: 'modal' }}
/>

The pinned conversation row in Messages stays host code (it is glue, not SDK); it builds its preview from loadAssistantPreview() and navigates to AssistantChat.

Glue 4: navigation call sites

Two functions wire the SDK into the host's navigation layer:

// NavigationContainer.onStateChange: hand over the deepest focused route.
// Feeds the nudge's browsing signal; keep your own analytics next to it.
onStateChange={(state) => {
  const route = resolveActiveRoute(state); // the host's existing resolver
  recordNavigation(route.name, route.params, route.key);
}}

// The push / in-app deep-link resolver: give the SDK first refusal.
// routeAgentLink handles '/assistant' links ("the team replied" pushes) and
// returns false for everything else, so the host's own switch keeps working.
export function navigateToLink(link: string, data?: Record<string, unknown>) {
  if (routeAgentLink(link, data)) return;
  // ...the host's existing cases...
}

Destinations

resolveDestination(destination, params) gets first refusal on every navigation the SDK wants to make. Destinations the SDK asks for today:

| Destination | Params | Fired by | SDK fallback when unhandled | |---|---|---|---| | card | { card_id } | reply card taps, driver navigate | navigate('CardDetail', { cardId }) (silent no-op without card_id) | | packs | {} | driver navigate | navigate('MainTabs', { screen: 'Packs' }) | | market | { query? } | driver navigate | navigate('MainTabs', { screen: 'Market', params: { agentQuery } }) | | assistant | push data | routeAgentLink('/assistant') | navigate(assistantScreenName) | | assistant_chat | {} | support-reply popup tap | navigate('AssistantChat') | | custom_pack_builder | {} | nudge CTA without driver actions | navigate('MainTabs', { screen: 'Packs', params: { openCustom: true } }) |

Unknown destination names return false from your resolver and the SDK treats them per the table (everything falls back to the reference route names, and an unknown route name is a React Navigation no-op).

Kill switches

Layered, outermost first:

  1. EXPO_PUBLIC_ASSISTANT_ENABLED at app build time, passed as the provider's enabled prop. Off = the stock app, nothing mounts.
  2. ASSISTANT_ENABLED on the relay (server env). Off = every relay route 404s and the SDK degrades to its fail-soft defaults.
  3. The remote agent_config flags, resolved per user by the relay's experiment endpoint and read at runtime on every surface: header_button, dm_row, nudge, driver_actions, screen_reporting, home_card, pack_reorder, clear_chat. Each gates exactly one surface or capability; all default off (ASSISTANT_FLAGS_OFF) until an assignment resolves, and a relay too old to send flags gets the legacy per-arm gating.
  4. Remote config copy and behavior knobs (greeting, nudge copy and cooldowns, telemetry sampling, driver_extra_destinations) are served per request; absent config renders the compiled defaults, which are the shipped literals.

Version-skew contract

A newer server must never crash an older app:

  • Unknown driver action types are dropped by the parser, never thrown on.
  • Deep-link paths the SDK does not know return false from routeAgentLink.
  • Unknown outbox message kinds render as plain text or are skipped.
  • navigate destinations outside the compiled set are only honored when the remote config allowlists them, and then only as bare route names (unknown names are a React Navigation no-op).
  • The compiled DENIED_SCREENS set is a code floor: no remote config, allowlist or provider prop can make the driver open those screens.
  • onSeedCustomPack absent or returning false drops the seed silently.

Cache compatibility

With keyPrefix: 'catchback' and a storage adapter over the existing disk cache, the SDK reads and writes the exact key strings the reference app used:

  • catchback_assistant_thread_v1_<userId | anon> (the on-device thread)
  • catchback_assistant_assignment_v2_<userId> (the resolved A/B assignment)
  • catchback_assistant_nudge_shown_v1_<userId> (nudge cooldown stamp)

Existing users keep their threads and assignments across the migration. Other adopters set their own prefix once and never change it.

Public surface

See src/index.ts for the full export list. The main groups: the provider (PalisadeAgent), surfaces (AssistantThread, AssistantChatScreen, AssistantHomeCard/AgentHomeCard), modal state (AssistantModalProvider, useAssistantModal), hooks (useAgentSession, useAnonAssistant, useAssistantArm, useAgentPackOrder), navigation glue (routeAgentLink, recordNavigation), the chat/driver/config lib functions the host's screens call directly, and the theme/component injection types (AgentTheme, AgentComponents, AgentMessageBubbleProps, AgentButtonProps, AgentSheetProps, AgentBubbleMessage).

The host API bridge (host_api_call)

The general seam for agent capabilities that act on the host app's own API (balance purchases, pack ripping, buyback sells, listing management). One glue callback, written once:

const agentApiBridge = async (path, init) => {
  const s = await getSession();
  if (!s) return null;
  const res = await fetch(`${ENV.API_URL}${path}`, {
    method: init?.method ?? 'GET',
    headers: { ...(init?.headers ?? {}), authorization: `Bearer ${s.accessToken}` },
    body: init?.body,
  });
  return { status: res.status, ok: res.ok, bodyText: await res.text().catch(() => '') };
};

<PalisadeAgent apiBridge={agentApiBridge} ... />

From then on, new capabilities are server-side toggles: the agent's reply carries {type: 'host_api_call', method, path, body, confirm, ...} and no client release is needed.

Safety is compiled into the SDK, not the glue, and none of it is config-overridable:

  • Same-origin relative paths only (no schemes, no //, no traversal, encoded or not), so a hostile config can never point the user's token at another origin.
  • A compiled deny floor (HOST_API_DENIED_PATTERNS): money rails in/out, transfers, gifts, auth/password, account deletion, admin. Never callable.
  • The remote allowlist behavior.api_call_allowlist (path prefixes) defaults to []: the whole action kind ships dark until the dashboard arms it.
  • Mutations always confirm: any non-GET renders a native confirm sheet (server-composed title/body/line items, config fallbacks) and runs only on a fresh user tap. Declining also drops the rest of that reply's actions. No sheet mounted = fail closed.
  • Outcomes are narrated (success/failure toasts) and reported through the host_api_call telemetry event (extend the ingest vocabulary server-side before relying on it).