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

@granular-software/sdk

v0.4.8

Published

TypeScript SDK and CLI for Granular - define, build, and deploy AI sandboxes

Downloads

1,284

Readme

@granular-software/sdk

The official TypeScript SDK for Granular.

Build AI-powered domain models with secure, isolated execution. Granular lets you define ontologies (classes, relationships), attach tools to them, and run AI-generated code that operates on typed domain objects — all in a sandboxed environment with per-user permissions.

Features

  • 🏗️ Domain Ontology — Define classes, properties, and relationships as a typed graph
  • 🔧 Class-Based Tools — Attach instance methods, static methods, and global tools with typed I/O
  • 🤖 Domain Synthesis — Auto-generated TypeScript classes with get({ path }), list(), relationship accessors, and typed methods
  • 🔒 Secure Execution — Run AI-generated code in isolated sandboxes
  • 👥 User Permissions — Control what each user can access
  • Real-time — WebSocket-based streaming and events
  • ↩️ Reverse RPC — Sandbox code calls tools that execute on your server

Documentation

Full documentation: docs.granular.dev

Installation

bun add @granular-software/sdk
# or
npm install @granular-software/sdk

Inside this monorepo, prefer the workspace package instead of installing from npm separately.

Endpoint Modes

By default, the SDK resolves endpoints like this:

  • Local mode (NODE_ENV=development): ws://localhost:8787/granular
  • Production mode (default): wss://cf-api-gateway.arthur6084.workers.dev/granular

Overrides:

  • SDK option: endpointMode: 'local' | 'production'
  • SDK option: apiUrl: 'ws://... | wss://...' (highest priority)
  • Env: GRANULAR_ENDPOINT_MODE=local|production
  • Env: GRANULAR_API_URL=... (highest priority)

CLI overrides:

  • granular --local <command>
  • granular --prod <command>
  • granular --env local|production <command>

Quick Start

import { Granular, type ManifestContent } from '@granular-software/sdk';

const granular = new Granular({ apiKey: process.env.GRANULAR_API_KEY });

// 1. Connect to sandbox for one of your app users
const env = await granular.connect({
  sandbox: 'my-sandbox',
  userId: 'user_123',
  permissions: ['agent'],
  name: 'Jane Doe',           // optional
  email: '[email protected]',  // optional
});

// 2. Define your domain ontology
const manifest: ManifestContent = {
  schemaVersion: 2,
  name: 'my-app',
  volumes: [{
    name: 'schema',
    scope: 'sandbox',
    imports: [{ alias: '@std', name: 'standard_modules', label: 'prod' }],
    operations: [
      // Define classes with typed properties
      {
        create: 'customer',
        extends: '@std/class',
        has: {
          name:  { type: 'string', description: 'Customer name' },
          email: { type: 'string', description: 'Email address' },
          tier:  { type: 'string', description: 'Subscription tier' },
        },
      },
      {
        create: 'order',
        extends: '@std/class',
        has: {
          total:  { type: 'number', description: 'Order total' },
          status: { type: 'string', description: 'Order status' },
        },
      },
      // Define relationships
      {
        defineRelationship: {
          left: 'customer', right: 'order',
          leftSubmodel: 'orders', rightSubmodel: 'customer',
          leftIsMany: true, rightIsMany: false,
        },
      },
    ],
  }],
};

await env.applyManifest(manifest);

// 3. Record object instances
await env.recordObject({
  className: 'customer',
  id: 'cust_42',
  label: 'Acme Corp',
  fields: { name: 'Acme Corp', email: '[email protected]', tier: 'enterprise' },
});

// 4. Register live effect handlers for effects already declared in the build manifest
await granular.registerEffects(env.sandboxId, [
  {
    name: 'get_billing_summary',
    description: 'Get billing summary for a customer',
    className: 'customer',           // Attached to Customer class
    // static omitted → instance method (receives object ID automatically)
    inputSchema: {
      type: 'object',
      properties: {
        period: { type: 'string', description: 'Billing period (e.g. "2024-Q1")' },
      },
    },
    outputSchema: {
      type: 'object',
      properties: {
        total:    { type: 'number', description: 'Total billed amount' },
        invoices: { type: 'number', description: 'Number of invoices' },
        period:   { type: 'string', description: 'The billing period' },
      },
      required: ['total', 'invoices'],
    },
    handler: async (customerId: string, params: any, ctx: any) => {
      // customerId comes from `this.id` in sandbox code
      console.log('Running for subject:', ctx.user.subjectId);
      return { total: 4250.00, invoices: 3, period: params?.period || 'current' };
    },
  },
]);

// 6. Submit a job — the sandbox gets fully typed OOP classes!
const job = await env.submitJob(`
  import { Customer } from './sandbox-tools';

  // list() discovers instances; get({ path }) hydrates a specific graph object
  const customers = await Customer.list();
  const acme = customers.find((customer) => customer.name === 'Acme Corp');
  if (!acme) throw new Error('Customer not found');
  console.log(acme.name);   // "Acme Corp"
  console.log(acme.email);  // "[email protected]"

  // Instance method — typed input AND output
  const billing = await acme.get_billing_summary({ period: '2024-Q1' });
  // billing.total, billing.invoices, billing.period are all typed

  // Navigate relationships
  const orders = await acme.get_orders();

  return { customer: acme.name, billing, orderCount: orders.length };
`);

const result = await job.result;

Effects must be declared ahead of time in the sandbox build manifest with withEffect. Live registration only makes already-declared effects available at runtime.

Core Flow

declare effects in build manifest → connect({ userId }) → recordObject() → registerEffects() → submitJob()
  1. connect() — Connect to a sandbox for a given userId, returning an Environment
  2. recordUser() — Optional explicit user upsert when you want the returned granularId
  3. applyManifest() — Define your domain ontology (classes, properties, relationships)
  4. recordObject() — Create/update instances of your classes with fields and relationships
  5. granular.registerEffects() — Register sandbox-scoped live handlers for effects declared in the build manifest
  6. submitJob() — Execute code in the sandbox that uses the auto-generated typed classes

Defining the Domain Ontology

Use applyManifest() to declare classes, typed properties, and relationships:

const manifest: ManifestContent = {
  schemaVersion: 2,
  name: 'library-app',
  volumes: [{
    name: 'schema',
    scope: 'sandbox',
    imports: [{ alias: '@std', name: 'standard_modules', label: 'prod' }],
    operations: [
      // Classes with typed properties
      {
        create: 'author',
        extends: '@std/class',
        has: {
          name:       { type: 'string',  description: 'Author full name' },
          birth_year: { type: 'number',  description: 'Year of birth' },
        },
      },
      {
        create: 'book',
        extends: '@std/class',
        has: {
          title:          { type: 'string', description: 'Book title' },
          isbn:           { type: 'string', description: 'ISBN number' },
          published_year: { type: 'number', description: 'Year published' },
        },
      },

      // Relationships (bidirectional, with cardinality)
      {
        defineRelationship: {
          left: 'author',  right: 'book',
          leftSubmodel: 'books',  rightSubmodel: 'author',
          leftIsMany: true,       rightIsMany: false,
          // → author.books (one-to-many), book.author (many-to-one)
        },
      },
    ],
  }],
};

await env.applyManifest(manifest);

Recording Object Instances

After defining the ontology, connect as a user and populate it with data:

const env = await granular.connect({
  sandbox: 'library-app',
  userId: 'user_123',
  permissions: ['agent'],
});

const tolkien = await env.recordObject({
  className: 'author',
  id: 'tolkien',               // Real-world ID (unique per class)
  label: 'J.R.R. Tolkien',
  fields: { name: 'J.R.R. Tolkien', birth_year: 1892 },
});
// tolkien.path → 'author_tolkien' (internal graph path, unique globally)
// tolkien.id   → 'tolkien'        (real-world ID)

const lotr = await env.recordObject({
  className: 'book',
  id: 'lotr',
  label: 'The Lord of the Rings',
  fields: { title: 'The Lord of the Rings', isbn: '978-0-618-64015-7', published_year: 1954 },
  relationships: { author: 'tolkien' },  // Real-world ID — SDK resolves it automatically
});

Cross-class ID uniqueness: Two objects of different classes can share the same real-world ID (e.g., an author "tolkien" and a publisher "tolkien"). The SDK derives unique graph paths internally (author_tolkien, publisher_tolkien) so they never collide.

Effect Definitions

Effects are declared in the manifest with withEffect, then their live handlers are registered at sandbox scope. Effects can be instance methods, static methods, or global functions. Both inputSchema and outputSchema use JSON Schema:

await granular.registerEffects(env.sandboxId, [
  // Instance method: called as `tolkien.get_bio({ detailed: true })`
  // Handler receives (objectId, params)
  {
    name: 'get_bio',
    description: 'Get biography of an author',
    className: 'author',              // Attached to Author class
    // static: false (default)        // Instance method
    inputSchema: {
      type: 'object',
      properties: {
        detailed: { type: 'boolean', description: 'Include full details' },
      },
    },
    outputSchema: {
      type: 'object',
      properties: {
        bio:    { type: 'string',  description: 'The biography text' },
        source: { type: 'string',  description: 'Source of the bio' },
      },
      required: ['bio'],
    },
    handler: async (id: string, params: any, ctx: any) => {
      return { bio: `Biography of ${id}`, source: 'database' };
    },
  },

  // Static method: called as `Author.search({ query: 'tolkien' })`
  // Handler receives (params) — no object ID
  {
    name: 'search',
    description: 'Search for authors',
    className: 'author',
    static: true,
    inputSchema: {
      type: 'object',
      properties: { query: { type: 'string' } },
      required: ['query'],
    },
    outputSchema: {
      type: 'object',
      properties: { results: { type: 'array' } },
    },
    handler: async (params: any, ctx: any) => {
      return { results: [`Found: ${params.query}`] };
    },
  },

  // Global tool: called as `global_search({ query: 'rings' })`
  // No className → standalone exported function
  {
    name: 'global_search',
    description: 'Search across everything',
    inputSchema: {
      type: 'object',
      properties: { query: { type: 'string' } },
      required: ['query'],
    },
    outputSchema: {
      type: 'object',
      properties: { results: { type: 'array' } },
    },
    handler: async (params: any, ctx: any) => {
      return { results: [`Result for: ${params.query}`] };
    },
  },
]);

Domain Synthesis (Auto-Generated Types)

After applying the manifest and publishing tools, the sandbox gets auto-generated TypeScript classes in ./sandbox-tools:

// What the sandbox sees (auto-generated):
export declare class Author {
  readonly id: string;
  readonly name: string;
  readonly birth_year: number;

  constructor(id: string, fields?: Record<string, any>);

  /** Get a cached Author by graph path, hydrating from the graph when needed */
  static get(query: { path: string; refresh?: boolean }): Promise<Author | null>;

  /** List known Author instances */
  static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Author[]>;

  /** Get biography of an author */
  get_bio(input?: { detailed?: boolean }): Promise<{ bio: string; source?: string }>;

  /** Search for authors (static) */
  static search(input: { query: string }): Promise<{ results?: any[] }>;

  /** Navigate to books (one_to_many) */
  get_books(): Promise<Book[]>;
}

export declare class Book {
  readonly id: string;
  readonly title: string;
  readonly isbn: string;
  readonly published_year: number;

  static get(query: { path: string; refresh?: boolean }): Promise<Book | null>;

  static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Book[]>;

  /** Navigate to author (many_to_one) */
  get_author(): Promise<Author | null>;
}

/** Search across everything */
export declare function global_search(input: { query: string }): Promise<{ results?: any[] }>;

The LLM or user writes code against these typed classes:

import { Author, Book, global_search } from './sandbox-tools';

const authors = await Author.list();
const tolkien = authors.find((author) => author.name === 'J.R.R. Tolkien');
if (!tolkien) throw new Error('Author not found');
console.log(tolkien.name);              // "J.R.R. Tolkien"

const bio = await tolkien.get_bio({ detailed: true });
console.log(bio.bio);                   // typed as string

const books = await tolkien.get_books();
for (const book of books) {
  console.log(book.title, book.isbn);   // typed properties
}

const results = await Author.search({ query: 'tolkien' });
const globalResults = await global_search({ query: 'rings' });

GraphQL API

Every environment has a built-in GraphQL API for direct graph queries:

// Authenticated automatically with your API key
const result = await env.graphql(
  `query { model(path: "author") { path label submodels { path label } } }`
);

// The endpoint is also available as a URL
console.log(env.apiEndpoint);

Framework Adapters

Framework-specific adapters are temporarily unavailable while the SDK build and release flow is being stabilized in the monorepo.

Examples

See examples/ for runnable code:

  • basic.ts — Ontology + class-based tools + domain synthesis + job execution
  • management.ts — Sandbox, permission profiles, and user management
  • interactive.ts — Human-in-the-loop prompts

API Reference

granular.recordUser(options)

Registers a user identity and their assigned permission profiles.

granular.connect(options)

Connects to a sandbox session for the specified user.

environment.applyManifest(manifest)

Defines domain ontology: classes (with typed properties), and relationships (with cardinality).

environment.recordObject(options)

Creates or updates a class instance with fields and relationships. Returns { path, id, created }.

environment.getRelationships(modelPath)

Returns relationship definitions for a given class.

environment.listRelated(modelPath, submodelPath)

Lists related instances through a relationship.

granular.registerEffects(sandboxId, effects)

Registers sandbox-scoped live handlers for effects declared in the sandbox build manifest. Effects can be instance methods (className set, static omitted), static methods (static: true), or global functions (no className).

environment.submitJob(code)

Submits code to be executed in the sandbox. The code imports typed classes from ./sandbox-tools.

environment.getDomainDocumentation()

Get auto-generated TypeScript class declarations. Pass this to LLMs to help them write correct code.

environment.graphql(query, variables?)

Execute a GraphQL query against the environment's graph. Authenticated automatically.

environment.on(event, handler)

Listen for events: 'effect:invoke', 'effect:result', 'job:status', 'stdout', etc. Legacy 'tool:*' aliases still exist internally but are no longer the primary model.

Environment.toGraphPath(className, id)

Convert a class name + real-world ID to a unique graph path ({className}_{id}).

Environment.extractIdFromGraphPath(graphPath, className)

Extract the real-world ID from a graph path by stripping the class prefix.

License

MIT