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

capybara-game-sdk

v0.0.125

Published

Game SDK for building multiplayer AI-powered games

Readme

Phaser + TypeScript Guide for Game SDK

This guide shows how to use the SDK modules in a Phaser.js project with TypeScript type support. It focuses on runtime usage in Phaser scenes and on wiring typed APIs for AI and multiplayer.

Install

npm install capybara-game-sdk

Quick Start (SDK init)

The SDK is a singleton. Initialize once (before creating Phaser game), then access from scenes via GameSDK.getInstance().

import { GameSDK } from "capybara-game-sdk";

async function bootSDK() {
  await GameSDK.init({
    config: {
      gameId: "your-game-id",
      environment: "dev",
      allowedOrigins: ["https://your-host.app"],
      devToken: "optional-dev-token",
      autoDefineProject: true,
      isMultiplayerEnabled: false,
    },
    onReady: () => {
      console.log("SDK ready");
    },
  });
}

void bootSDK();

Phaser integration pattern

Use this pattern inside a Phaser scene.

import { GameSDK } from "capybara-game-sdk";

export class MainScene extends Phaser.Scene {
  private sdk = GameSDK.getInstance();

  create() {
    const userPromise = this.sdk.auth.getCurrentUser();
    void userPromise;
  }
}

If you use AI agents in a scene, register them for automatic cleanup on scene shutdown:

import { registerSceneAgent } from "capybara-game-sdk";

const ai = sdk.ai;
const agent = await ai.createAgent(config);
registerSceneAgent(this, ai, agent);

Module Overview (Public API)

Modules are available from the GameSDK instance:

const sdk = GameSDK.getInstance();
sdk.auth;
sdk.payments;
sdk.configModule;
sdk.entitlements;
sdk.assets;
sdk.storage;
sdk.world;
sdk.ai;
sdk.multiplayer; // only when isMultiplayerEnabled is true

Internal-only modules (not exported from core/index.ts):

  • ApiClient, ParentBridge (used internally by SDK)

React hooks are exported, but are optional and not required in Phaser.


AuthModule

Handles login with the parent app (iframe host). The SDK auto-requests login on startup if no user is present.

const user = await sdk.auth.getCurrentUser();
const token = await sdk.auth.getBearerToken();
const loggedIn = sdk.auth.isAuthenticated();

const unsubscribe = sdk.auth.onAuthChange((nextUser) => {
  console.log("auth changed", nextUser);
});

await sdk.auth.requestLogin();
await sdk.auth.logout();

| Method | Return | Description | | ------------------- | ------------------------- | ----------------------------------------------- | | getCurrentUser() | Promise<User> | Get the authenticated user | | getBearerToken() | Promise<string \| null> | Get the bearer token | | isAuthenticated() | boolean | Synchronous check if a user is logged in | | onAuthChange(cb) | () => void | Subscribe to auth changes (returns unsubscribe) | | requestLogin() | Promise<void> | Trigger login via parent UI | | logout() | Promise<void> | Logout the current user |

Type support: User from capybara-game-sdk.


PaymentsModule

Charge credits and check if a charge is allowed. Balance is intentionally masked in minimal mode.

const canCharge = await sdk.payments.canCharge(50);
if (canCharge) {
  const result = await sdk.payments.charge(50, "Unlock bonus mode");
  if (!result.success) {
    console.warn(result.error);
  }
}

const unsubscribe = sdk.payments.onBalanceChange((balance) => {
  console.log("balance updated", balance.credits);
});

const history = await sdk.payments.getHistory();

| Method | Return | Description | | ----------------------------- | ------------------------ | -------------------------------------------------- | | canCharge(amount) | Promise<boolean> | Check if a charge is allowed | | charge(amount, description) | Promise<PaymentResult> | Charge credits | | onBalanceChange(cb) | () => void | Subscribe to balance changes (returns unsubscribe) | | getHistory() | Promise<Transaction[]> | Session-only transaction history |

Deprecated: getBalance() and requestPurchase() (use Entitlements).


ConfigModule

Define and fetch game entitlements config (gating rules used by the server).

import { defineEntitlementsConfig } from "capybara-game-sdk";

const entitlements = defineEntitlementsConfig({
  creditUsd: 0.01,
  features: {
    "level:3": { cost: 200, type: "unlock", serverValidated: true },
    "skin:cyber": { cost: 100, type: "unlock", serverValidated: true },
    "ai:hint": { cost: 10, type: "consumable", serverValidated: true },
  },
  consumables: {
    "ai:hint": { cost: 10, type: "consumable", serverValidated: true },
  },
});

const config = await sdk.configModule.get();

await sdk.configModule.defineProject({
  gameId: "your-game-id",
  entitlements,
});

const remoteConfig = await sdk.configModule.loadProjectConfig(
  "https://example.com/config.json",
);

| Method | Return | Description | | ------------------------ | -------------------------------- | ---------------------------------- | | get() | Promise<TEntitlements \| null> | Fetch current entitlements config | | defineProject(config) | Promise<void> | Push project config (dev mode) | | loadProjectConfig(url) | Promise<GameProjectConfig> | Load config from a remote JSON URL |

defineProject uses dev token when provided and is intended for dev mode.


EntitlementsModule

Unlocks features with server validation and fetches gated manifests.

const unlock = await sdk.entitlements.unlock("skin:cyber");
if (unlock.success) {
  console.log("Unlocked", unlock.unlockToken);
}

const owns = await sdk.entitlements.has("skin:cyber");
const list = await sdk.entitlements.list();

const purchase = await sdk.entitlements.purchaseConsumable("ai:hint", 1);
const consume = await sdk.entitlements.consumeConsumable("ai:hint", 1);
const inventory = await sdk.entitlements.inventory();

const manifest = await sdk.entitlements.resolveManifest("level:3");
const unlocked = await sdk.entitlements.unlockWithManifest("skin:cyber");

const isValid = await sdk.entitlements.verify("skin:cyber");
const tokenValid = await sdk.entitlements.verifyToken(unlock.unlockToken!);

| Method | Return | Description | | --------------------------------- | --------------------------------------------- | --------------------------------------------- | | unlock(featureId) | Promise<EntitlementUnlockResult> | Unlock a permanent feature (server validated) | | has(featureId) | Promise<boolean> | Check if a feature is unlocked | | list() | Promise<EntitlementGrant[]> | List all unlocked features | | purchaseConsumable(itemId, qty) | Promise<ConsumablePurchaseResult> | Purchase consumables | | consumeConsumable(itemId, qty) | Promise<ConsumableUseResult> | Consume a purchased consumable | | inventory() | Promise<EntitlementInventory> | Get consumable inventory | | resolveManifest(featureId) | Promise<AssetManifest> | Fetch asset manifest for a gated feature | | unlockWithManifest(featureId) | Promise<EntitlementUnlockAndManifestResult> | Unlock and return asset manifest in one call | | verify(featureId) | Promise<boolean> | Server-side verification of an entitlement | | verifyToken(token) | Promise<boolean> | Server-side verification of an unlock token |


AssetsModule

Fetch signed manifests for gated assets and load them in Phaser.

import type { AssetEntry } from "capybara-game-sdk";

const manifest = await sdk.assets.getManifest("skin:cyber");
manifest.assets.forEach((asset: AssetEntry) => {
  if (asset.url) {
    this.load.image(asset.key || asset.id, asset.url);
  }
});

this.load.once("complete", () => {
  // use the assets
});

this.load.start();

StorageModule

Persist per-user game data (5MB max). Safe for saves, progress, settings.

type SaveData = { level: number; coins: number };

await sdk.storage.save<SaveData>({ level: 3, coins: 120 });
const data = await sdk.storage.load<SaveData>();

if (data) {
  console.log(data.level);
}

await sdk.storage.clear();
const exists = await sdk.storage.exists();
const size = await sdk.storage.getSize();

WorldStateModule

Server-side shared state. Useful for live event flags or global counters.

await sdk.world.set<number>("bossDefeats", 42, "global");
const entry = await sdk.world.get<number>("bossDefeats", "global");

Scopes: "user" | "room" | "global" | "session" (type MemoryScope).


AIModule + Agent

Create typed agents that react to game events. For Phaser, define a game event map interface so you get type-checked event payloads.

import { createAction } from "capybara-game-sdk";
import { z } from "zod";

type GameEvents = {
  enemySpotted: { enemyId: string; distance: number };
  itemCollected: { itemId: string; count: number };
};

const ai = sdk.ai as import("capybara-game-sdk").AIModule<GameEvents>;

const agent = await ai.createAgent({
  name: "npc-guard",
  identity: {
    systemPrompt: "You are a guard NPC.",
    model: "gpt-4o-mini",
  },
  context: [
    {
      name: "World",
      run: async () => "You are in a medieval town.",
    },
  ],
  actions: {
    playAnimation: createAction({
      description: "Play a Phaser animation",
      schema: z.object({ key: z.string() }),
      handler: async ({ key }) => {
        this.anims.play(key);
      },
    }),
  },
  memory: {
    enabled: true,
    scope: "user",
    summarizeAfterChars: 4000,
  },
  lifecycle: {
    mode: "REACTIVE",
    events: ["enemySpotted", "itemCollected"],
    maxTurns: 2,
  },
});

registerSceneAgent(this, ai, agent);

ai.broadcast("enemySpotted", { enemyId: "goblin-1", distance: 12 });

AIModule methods

| Method | Return | Description | | ------------------------------- | ---------------- | ----------------------------------------------------- | | createAgent(config) | Promise<Agent> | Create and register a new agent | | getAgent(agentId) | Agent \| null | Look up an agent by ID (the agent's name) | | getAllAgents() | Agent[] | Get every registered agent | | removeAgent(agentId) | void | Destroy and unregister an agent | | broadcast(eventName, payload) | void | Type-safe event broadcast to listening agents | | clear() | void | Destroy and remove all agents | | getAgentCount() | number | Number of active agents | | subscribe(cb) | () => void | Subscribe to agent list changes (returns unsubscribe) |

Notes:

  • registerSceneAgent ensures agents are removed on scene shutdown.
  • createAction provides type-safe schema validation for actions.
  • An agent's ID equals its name from the config.

Agent instance

createAgent returns an Agent instance. Use it to drive conversations, inspect state, and control lifecycle.

Properties

| Property | Type | Description | | ------------ | ------------------------- | ---------------------------------------- | | id | string | Agent identifier (same as config.name) | | config | AgentConfig | The config passed to createAgent | | isThinking | boolean | true while the agent is mid-turn | | lastLog | string \| null | Last assistant message or status | | state | Record<string, unknown> | Arbitrary agent state | | messages | AgentMessage[] | Local transcript snapshot | | scope | MemoryScope | Resolved memory scope |

Methods

// Chat sends a user message and triggers a think cycle
await agent.chat("Where is the treasure?");

// Think runs context resolution + LLM turn (optionally with an event)
await agent.think("optional prompt", event);

// Execute a named action manually
const result = await agent.executeAction("playAnimation", { key: "wave" });

// Merge partial data into agent.state and notify subscribers
agent.updateState({ health: 80 });

// Subscribe to any state/thinking changes (returns unsubscribe)
const unsub = agent.subscribe(() => {
  console.log(agent.isThinking, agent.lastLog);
});

// Autonomous lifecycle control
agent.startAutonomous(); // begin interval-based thinking
agent.stopAutonomous(); // stop the interval

// Tear down (stops autonomous loop, called automatically by removeAgent)
agent.destroy();

| Method | Return | Description | | ----------------------------- | ------------------------- | ------------------------------------------ | | chat(content) | Promise<void> | Send user message and trigger think | | think(message?, event?) | Promise<void> | Run a think cycle | | executeAction(name, params) | Promise<unknown> | Execute a registered action by name | | updateState(partial) | void | Merge into agent.state and notify | | subscribe(cb) | () => void | Subscribe to changes (returns unsubscribe) | | resolveContext(event?) | Promise<string \| null> | Build context prompt from context units | | startAutonomous() | void | Start interval-based think loop | | stopAutonomous() | void | Stop the autonomous loop | | destroy() | void | Clean up resources |


Agent memory

Agents support server-side memory that persists across think cycles. Configure it via the memory field when creating an agent.

import type { AgentMemoryConfig, MemoryScope } from "capybara-game-sdk";

const agent = await ai.createAgent({
  name: "shopkeeper",
  identity: { systemPrompt: "You are a shopkeeper.", model: "gpt-4o-mini" },
  context: [],
  actions: {},
  memory: {
    enabled: true, // turn on server-side memory
    scope: "user", // persist per user (default when omitted)
    summarizeAfterChars: 4000, // auto-summarize after this many chars
  },
  lifecycle: { mode: "REACTIVE", events: [] },
});

AgentMemoryConfig

| Field | Type | Default | Description | | --------------------- | ------------- | ----------- | --------------------------------------------------------------- | | enabled | boolean | undefined | Enable server-side memory for this agent | | scope | MemoryScope | "user" | Persistence scope ("user", "room", "global", "session") | | summarizeAfterChars | number | undefined | Auto-summarize conversation after this character count |

How scope is resolved

The agent's effective scope is determined by: config.scope ?? config.memory?.scope ?? "user". This means you can set scope at the top level of the config or inside memory.

Related types

The following types are exported from capybara-game-sdk and relate to the memory system:

  • AgentMemoryConfig -- the config object shown above
  • MemoryEntry -- a stored memory record (id, agentId, scope, text, tags?, createdAt, metadata?)
  • MemoryQuery -- search parameters (query, scope?, limit?, tags?)
  • MemorySummaryOptions -- options for summary retrieval (scope?)
  • MemoryScope -- "user" | "room" | "global" | "session"

Memory storage, search, and summarization are handled server-side. The SDK sends the memory config when creating the agent and the server manages the rest during think cycles.


MultiplayerModule (Colyseus)

Provides typed rooms with Zod schema validation and type inference.

import { z } from "zod";
import type { GameSchema } from "capybara-game-sdk";

const schema: GameSchema<
  { players: Record<string, { x: number; y: number }> },
  { move: { x: number; y: number } }
> = {
  name: "arena",
  stateSchema: z.object({
    players: z.record(z.object({ x: z.number(), y: z.number() })),
  }),
  messageSchemas: {
    move: z.object({ x: z.number(), y: z.number() }),
  },
};

const multiplayer = sdk.multiplayer;
if (multiplayer) {
  const room = await multiplayer.joinOrCreate(schema);

  room.onStateChange((state) => {
    // state is fully typed
    Object.entries(state.players).forEach(([id, pos]) => {
      // update Phaser sprites
    });
  });

  room.onMessage("move", (payload) => {
    console.log(payload.x, payload.y);
  });

  room.send("move", { x: 12, y: 8 });
}

MultiplayerModule methods

| Method | Return | Description | | -------------------------------------- | -------------------- | -------------------------------------------- | | joinOrCreate(schema, options?) | Promise<TypedRoom> | Join or create a room with schema validation | | create(schema, options?) | Promise<TypedRoom> | Create a new room | | joinById(roomId, roomName, options?) | Promise<TypedRoom> | Join an existing room by ID | | leave(roomId, consented?) | Promise<void> | Leave a room | | leaveAll() | Promise<void> | Leave all rooms | | getRoom(roomId) | Room \| null | Get a cached Colyseus room by ID | | registerSchema(schema) | void | Pre-register a schema for validation |

TypedRoom interface

The TypedRoom returned by joinOrCreate, create, and joinById exposes:

| Property / Method | Type | Description | | --------------------- | --------------- | -------------------------------------- | | room | Room<TState> | Underlying Colyseus room | | state | TState | Current validated state (always fresh) | | sessionId | string | Local session ID | | id | string | Room ID | | send(type, payload) | void | Send a validated message | | onMessage(type, cb) | void | Listen for a message type | | onStateChange(cb) | void | Listen for state changes | | leave(consented?) | Promise<void> | Leave the room |


Phaser helper utilities

registerSceneAgent(scene, ai, agent)

  • Cleans up agents when the Phaser scene shutdown or destroy events fire.
  • Use it anytime you create agents inside a scene.

TypeScript tips

  • All public types are exported from capybara-game-sdk (User, GameSchema, MemoryScope, etc.).
  • For AI, define a GameEvents map and use it in AIModule<GameEvents> and AgentConfig<GameEvents> for full payload checking.
  • For multiplayer, use GameSchema with Zod to infer state and message types.

Internal vs external modules

  • External, public modules are exported from core/index.ts and are documented above.
  • Internal modules (ApiClient, ParentBridge) are used by the SDK runtime and are not intended for direct use in Phaser projects.