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

@uluops/registry-sdk

v0.47.1

Published

SDK for the UluOps Registry API - manage AI workflow definitions

Readme

UluOps · Operating Intelligence as Infrastructure


@uluops/registry-sdk

npm version License: MIT Node.js TypeScript

TypeScript SDK for the UluOps Registry API. Manage AI workflow definitions including agents, commands, workflows, and pipelines.

Quick Start

Note: Examples use TypeScript syntax. Run with tsx (npx tsx script.ts) or compile with tsc first. A running Registry API server is required — see Environment Variables to configure the base URL.

Node.js (Recommended)

Set your API key in the environment (or a .env file), then use createClientFromEnvironment() to auto-discover credentials:

export ULUOPS_API_KEY=ulr_your-api-key-here
import { createClientFromEnvironment } from '@uluops/registry-sdk/config';

const client = createClientFromEnvironment();

// List agent definitions
const { definitions: agents } = await client.definitions.list({ type: 'agent' });

// Get a specific definition
const def = await client.definitions.get('agent', 'code-validator', '1.0.0');

// Create a new definition
const newDef = await client.definitions.create('agent', 'my-agent', {
  yaml: 'agent:\n  interface:\n    name: my-agent\n    version: 1.0.0',
});

Browser / Explicit Config

The RegistryClient constructor is browser-safe and does not read environment variables. Pass credentials directly:

import { RegistryClient } from '@uluops/registry-sdk';

const client = new RegistryClient({
  apiKey: process.env.ULUOPS_API_KEY, // or pass from your backend
});

Security: Never hardcode API keys in source code. Use environment variables, secret managers, or server-side proxies to inject credentials at runtime.

Table of Contents

Features

  • Full API Coverage: Access all registry endpoints across 13 operation domains
  • Browser Compatible: Constructor is browser-safe — use in Next.js, React, or any browser bundler
  • Type-Safe: Complete TypeScript definitions for API operations with Zod runtime validation
  • Dual Authentication: API key (preferred) and JWT session support
  • Automatic Retries: Exponential backoff for transient errors (502, 503, 504, 429, network failures)
  • Error Hierarchy: Typed errors for precise error handling
  • Server-Side Normalization: Request runtime-ready normalized definitions via { normalize: true } — transforms computed server-side
  • Subpath Exports: Import only what you need (/types, /errors, /config)

Installation

Note: This package is ESM-only. CJS environments (require()) are not supported. Ensure your project uses "type": "module" or an ESM-compatible bundler.

# npm
npm install @uluops/registry-sdk

# yarn
yarn add @uluops/registry-sdk

# pnpm
pnpm add @uluops/registry-sdk

Requirements:

  • Node.js 18.0.0 or higher (server-side)
  • Any modern browser with fetch support (client-side)
  • TypeScript 5.0+ (for TypeScript users)

Authentication

The SDK supports two authentication methods:

API Key Authentication (Recommended)

API keys provide persistent authentication without session management. Keys must start with the ulr_ prefix.

Node.js — use environment discovery (reads ULUOPS_API_KEY from env, .env files, and ~/.uluops/credentials.json):

import { createClientFromEnvironment } from '@uluops/registry-sdk/config';

const client = createClientFromEnvironment();
console.log(client.isAuthenticated()); // true
console.log(client.getAuthType()); // 'api_key'

Browser / Explicit — pass the key directly (do not hardcode in source):

import { RegistryClient } from '@uluops/registry-sdk';

const client = new RegistryClient({
  apiKey: process.env.ULUOPS_API_KEY,
});

Session-Based Authentication

For interactive applications, use login() to obtain a session token programmatically.

Note: Constructing new RegistryClient() without credentials is valid — the client starts unauthenticated and becomes authenticated after login() succeeds. Any API call made before login will fail with UnauthorizedError.

// Login with email/password — no API key required
const sessionClient = new RegistryClient();
const { sessionToken, expiresAt } = await sessionClient.login('[email protected]', 'password');
// The client is now authenticated — subsequent requests use the session token

// Or pass an existing token directly
const tokenClient = new RegistryClient({
  sessionToken: 'your-jwt-token',
});

// Clear the local session when done (does not revoke the token server-side)
sessionClient.clearLocalSession();

The auth URL defaults to production (https://api.uluops.ai/api/v1).

Validating Credentials

Use the config utilities to check credentials before constructing a client:

import { isApiKey, validateCredentials, API_KEY_PREFIX } from '@uluops/registry-sdk/config';

const key = process.env.ULUOPS_API_KEY;

// Check that credentials are present (throws ValidationError if missing)
try {
  validateCredentials({ apiKey: key });
} catch (error) {
  console.error('No credentials found:', error.message);
  process.exit(1);
}

// Validate API key prefix format (returns boolean)
if (!isApiKey(key!)) {
  console.error(`Invalid API key format. Keys must start with '${API_KEY_PREFIX}'`);
  process.exit(1);
}

Credential Priority Chain

When using createClientFromEnvironment() (see Node.js Environment Discovery), credentials are loaded in the following order:

  1. Explicit arguments: apiKey, sessionToken passed to the function
  2. Environment variables: ULUOPS_API_KEY, ULUOPS_SESSION_TOKEN
  3. Local .env file: In the current working directory
  4. Global credentials: ~/.uluops/credentials.json

Note: The new RegistryClient() constructor uses only the config you pass directly. It does not read environment variables or credential files — this keeps it browser-safe. Use createClientFromEnvironment() for automatic credential discovery in Node.js.

TypeScript Support

The SDK is written in TypeScript with full type definitions. Import types directly:

// Main client
import { RegistryClient, type RegistryClientConfig } from '@uluops/registry-sdk';

// Types only
import type {
  Definition,
  DefinitionType,
  DefinitionStatus,
  DefinitionRef,        // lightweight { type, name, version } reference, embedded in analytics/execution results
  ValidationFieldError,
  Model,
  ModelCost,            // pricing block, USD per million tokens; null = unpriced
  PublicUser,
} from '@uluops/registry-sdk';

// Errors only
import {
  RegistryApiError,
  ValidationError,
  NotFoundError,
  RateLimitError,
} from '@uluops/registry-sdk/errors';

// Config utilities
import { loadCredentials, DEFAULT_BASE_URL } from '@uluops/registry-sdk/config';

Package Exports

| Export Path | Contents | Browser Safe* | |------------|----------|:---:| | @uluops/registry-sdk | Main RegistryClient, RegistryHttpClient, auth strategies | Yes | | @uluops/registry-sdk/types | TypeScript types (PascalCase), Zod runtime schemas (*Schema suffix), enum arrays (SCREAMING_SNAKE) | Yes | | @uluops/registry-sdk/errors | Error classes and utilities | Yes | | @uluops/registry-sdk/config | Configuration loaders, createClientFromEnvironment, constants | No (Node.js) | | @uluops/registry-sdk/config/constants | SDK_VERSION, MAX_YAML_SIZE, ERROR_CODES, HTTP_STATUS, ENV_VARS, etc. | Yes |

* Browser Safe = no Node.js built-ins required (importable in any browser bundler). API calls from browsers require a server-side proxy — see Browser Usage.

API Compatibility

This SDK targets the UluOps Registry API v1 (/api/v1). The SDK version follows semver:

  • Patch (0.1.x): Bug fixes, no API changes
  • Minor (0.x.0): New features, backward-compatible
  • Major (x.0.0): Breaking changes (method signatures, removed endpoints)

Note (pre-1.0): While this project is pre-1.0, minor releases may include breaking changes when the impact is demonstrably contained — for example, when fields were typed but never populated by the live API (see v0.31.0 / R12 envelope rewrite). The CHANGELOG ### Changed / ### Removed sections always carry the exact migration steps. Post-1.0 we will follow strict semver.

The base URL defaults to https://api.uluops.ai/api/v1/registry.

API Reference

Client Configuration

const client = new RegistryClient({
  // Authentication (choose one)
  apiKey: process.env.ULUOPS_API_KEY,  // API key (preferred)
  sessionToken: 'jwt-token',   // Existing session token
  email: '[email protected]',   // Email for session auth (requires password)
  password: 'secret',          // Password for session auth (requires email)

  // Connection settings
  timeout: 30000,              // Request timeout in ms (default: 30000)
  retries: 3,                  // Retry count for transient errors (default: 3)
  orgSlug: 'my-org',           // Organization slug for multi-tenancy
  debug: false,                // Enable debug logging

  // Callbacks
  onTokenRefresh: (token) => { /* handle token refresh */ },
  onRateLimitApproaching: (info) => {
    console.warn(`Rate limit: ${info.remaining}/${info.limit} remaining, resets ${info.reset}`);
  },
  rateLimitThreshold: 0.1,  // Fire when <10% remaining (default)
  onRetry: ({ attempt, maxAttempts, error, delayMs }) => {
    console.warn(`Retry ${attempt}/${maxAttempts} after ${delayMs}ms: ${error.message}`);
  },
  onSecurityEvent: (event) => { /* route to telemetry — see "Security Events" */ },
});

Security Events

Since @uluops/[email protected], the client exposes a structured security-event channel. Pass onSecurityEvent to route the security-relevant events the client already observes to your telemetry sink. The handler is forwarded to the underlying sdk-core client; delivery is best-effort and fire-and-forget (a throwing handler is caught and logged, never propagated into request flow).

import { RegistryClient, type SecurityEvent } from '@uluops/registry-sdk';

const client = new RegistryClient({
  apiKey: process.env.ULUOPS_API_KEY,
  onSecurityEvent: (event: SecurityEvent) => {
    switch (event.type) {
      case 'auth_failure':           siem.alert('auth_rejected', { authType: event.authType, requestId: event.requestId }); break;
      case 'redirect_rejected':      siem.alert('redirect_blocked', { origin: event.baseUrl }); break;
      case 'token_refresh_failed':   siem.alert('session_refresh_failed', {}); break;
      case 'auth_strategy_replaced': siem.alert('credential_swapped', { from: event.previousType, to: event.newType }); break;
    }
  },
});

| Event type | Fires when | |--------------|-----------| | auth_failure | A sent credential is rejected with 401 (and not transparently refreshed) | | redirect_rejected | The configured origin returns a 3xx the SDK refuses to follow | | token_refresh_failed | A session token refresh (re-login) is rejected | | auth_strategy_replaced | The live credential is swapped via setAuthStrategy |

The registry SDK runs a separate authBaseUrl (login/refresh delegates to the ops API), so a redirect_rejected on the credential-carrying login POST is reported against authBaseUrl. A blocked redirect throws RedirectError (re-exported from @uluops/registry-sdk/errors, non-retryable — catch it with isRedirectError(e) where you previously caught NetworkError). The SecurityEvent union and member types are exported from the package root.

Client Instance Methods

| Method | Returns | Description | |--------|---------|-------------| | isAuthenticated() | boolean | Check if credentials are configured and valid | | getAuthType() | 'api_key' \| 'session' \| null | Get the authentication strategy in use | | login(email, password) | Promise<LoginResult> | Login with email/password via the ops API | | clearLocalSession() | void | Clear the local session token (no server-side revocation) | | getHttpClient() | RegistryHttpClient | Access the underlying HTTP client for custom requests |


Definitions (client.definitions)

Manage AI workflow definitions (agents, commands, workflows, pipelines).

list(query?)

List definitions with optional filters.

| Parameter | Type | Description | |-----------|------|-------------| | type | DefinitionType | Filter by type ('agent', 'command', 'workflow', 'pipeline') | | status | DefinitionStatus | Filter by status ('draft', 'published', 'deprecated', 'archived') | | domain | Domain | Filter by domain | | authorId | string | Filter by author user ID | | visibility | Visibility | Filter by visibility | | search | string | Text search across name and description | | tag | string \| string[] | Filter by tag(s) | | isFork | boolean | true = only forks, false = only originals | | authorshipType | AuthorshipType | Filter by authorship ('human', 'agent', 'collaborative', 'automated') | | agentType | AgentType | Filter by agent type | | tier | Tier | Filter by subscription tier | | sortBy | SortField | Sort field — 'name', 'createdAt', 'updatedAt', 'executionCount' (total runs), 'uniqueExecutionCount' (distinct users) | | sortOrder | SortOrder | 'asc' or 'desc' | | limit | number | Max results (default: 50, max: 200) | | offset | number | Pagination offset |

const agents = await client.definitions.list({
  type: 'agent',
  status: 'published',
  limit: 20,
});

List items carry risk scalars (since 0.44.0): each DefinitionListItem includes riskLevel, scanStatus, and deepStatus — the safety verdict denormalized to the list grain, so agents and tools can rank or warn during discovery without a per-definition get. The P6 sentinel rule applies here too: gate on isListVerdictTrustworthy(item) before treating riskLevel: 'none' as clean — an absent/null triple means not yet scanned (pending), and 'none' beside scanStatus: 'failed' means could not determine, never clean.

import { isListVerdictTrustworthy } from '@uluops/registry-sdk';

for (const item of agents.definitions) {
  if (!isListVerdictTrustworthy(item)) {
    console.warn(`${item.name}: scan pending/incomplete — verdict unknown`);
  } else if (item.riskLevel !== 'none') {
    console.warn(`${item.name}: ${item.riskLevel} risk signals — inspect riskProfile via get()`);
  }
}

Verdict currency (since 0.45.0): list items also carry analyzerStale: boolean | null — whether the verdict predates the registry's current analyzer (detector set), computed server-side at read time. null = never scanned, true = stale (the registry already demotes such rows in search ordering), false = current. Informational only: it is deliberately not part of isListVerdictTrustworthy — a stale verdict is still a real verdict. Surface it as a disclosure:

if (item.analyzerStale) {
  console.info(`${item.name}: risk verdict predates the current analyzer — may be outdated`);
}

get(type, name, version?, options?)

Get a definition by type, name, and optional version.

| Option | Type | Description | |--------|------|-------------| | includeYaml | boolean | Include raw YAML content in response | | includeRuntime | boolean | Include rendered markdown in response | | includeRefs | boolean | Include dependency references |

// Get latest version
const def = await client.definitions.get('agent', 'code-validator');

// Get specific version
const def = await client.definitions.get('agent', 'code-validator', '1.0.0');

// Include YAML and rendered markdown
const full = await client.definitions.get('agent', 'code-validator', '1.0.0', {
  includeYaml: true,
  includeRuntime: true,
});

create(type, name, body)

Create a new draft definition.

const def = await client.definitions.create('agent', 'my-agent', {
  yaml: `
agent:
  interface:
    name: my-agent
    version: 1.0.0
    description: My custom agent
  `,
  visibility: 'public',
});

update(type, name, version, body)

Update an existing draft definition.

const def = await client.definitions.update('agent', 'my-agent', '1.0.0', {
  yaml: updatedYaml,
});

delete(type, name, version)

Delete a draft definition. Published definitions cannot be deleted.

await client.definitions.delete('agent', 'my-agent', '1.0.0');

publish(type, name, version)

Publish a draft definition to make it available.

Returns PublishResult ({ definition, warnings }) since 0.29.0 — not a bare Definition. warnings is always an array (possibly empty) of non-fatal publish warnings (e.g. TRANSLATION_FAILED, safety-scan failures).

const result = await client.definitions.publish('agent', 'my-agent', '1.0.0');
console.log(result.definition.status); // 'published'
if (result.warnings.length > 0) {
  console.warn('Publish warnings:', result.warnings.map((w) => w.code));
}

deprecate(type, name, version, body)

Deprecate a published definition.

const def = await client.definitions.deprecate('agent', 'my-agent', '1.0.0', {
  reason: 'Replaced by my-agent-v2',
  successor: '[email protected]',
});

archive(type, name, version)

Archive a deprecated definition. This is a terminal state that removes the definition from discovery.

await client.definitions.archive('agent', 'my-agent', '1.0.0');

Safety Analysis (definition.riskProfile)

Definitions may carry a riskProfile populated by the registry's safety scanner at publish time. It is undefined/null when the version has not been scanned. All safety types are exported from the package root and from @uluops/registry-sdk/types.

Gate on isVerdictTrustworthy before trusting the verdict. A profile whose sync scan failed to complete still carries aggregateRiskLevel: 'none' — but that 'none' is a sentinel meaning "could not determine", not a clean verdict. The same applies when the background deep analysis errored (deep.status: 'error'): the aggregate stays at the sync level, so a sync-clean definition whose deep audit crashed would read as clean. Reading aggregateRiskLevel directly would render either crash as safe. The exported isVerdictTrustworthy(profile) predicate returns false for a failed sync scan (scanStatus: 'failed'), an errored deep analysis (deep.status: 'error'), or an absent profile — treat those as "not fully analyzed", never as clean. (deep: null — skipped or not yet run — stays trusted.)

import {
  isVerdictTrustworthy,
  type RiskProfile,
  type RiskLevel,
  type SafetySignal,
} from '@uluops/registry-sdk';

const def = await client.definitions.get('agent', 'my-agent', '1.0.0');

if (!isVerdictTrustworthy(def.riskProfile)) {
  // Never scanned, or the scan failed — the verdict cannot be trusted.
  console.warn('Safety verdict unavailable — definition not fully analyzed.');
} else if (def.riskProfile!.aggregateRiskLevel !== 'none') {
  for (const signal of def.riskProfile!.sync.signals) {
    console.warn(`[${signal.severity}] ${signal.title} — ${signal.detail}`);
  }
}

| riskProfile field | Type | Description | |---------------------|------|-------------| | aggregateRiskLevel | RiskLevel ('none' \| 'medium' \| 'high') | Combined risk across sync + deep analysis. A sentinel ('none'), not a verdict, when scanStatus is 'failed' or deep.status is 'error' — gate with isVerdictTrustworthy. | | scanStatus | 'complete' \| 'failed' (optional) | Sync scan outcome. Absent on legacy rows → treat as 'complete'. When 'failed', the verdict could not be determined. | | scanFailedReason | 'parse_error' \| 'timeout' \| 'internal' (optional) | Present when scanStatus === 'failed'. | | sync | SyncScanResult | Synchronous publish-time scan: capabilities, signals[], riskLevel | | deep | DeepAnalysisResult \| null | Background deep analysis (findings[]), null until it runs. deep.status: 'error' means the deep verdict could not be determined (empty findings is a sentinel, not clean). | | lastUpdated | string | ISO timestamp of the most recent scan |

Risk levels are none, medium, or high — there is deliberately no low (a signal always means something). Risk reflects evidence of misuse, not capability count or provenance.

At the list grain (since 0.44.0), the same verdict travels as three flat scalars on DefinitionListItemriskLevel, scanStatus, deepStatus — gated by isListVerdictTrustworthy(item) (the list-grain twin of isVerdictTrustworthy). The full riskProfile remains a detail-fetch field.


Versions (client.versions)

Manage definition version history.

list(type, name, options?)

List all versions of a definition. Each item carries per-version author identity: createdBy (user ID), createdByName (resolved username, null for deleted users), and provenance (the version's contributor record) — so the version history tells the full authorship story even when different org members published different versions.

const { versions } = await client.versions.list('agent', 'code-validator', {
  limit: 20,
  offset: 0,
});
for (const v of versions) {
  console.log(`${v.version} by ${v.createdByName ?? v.createdBy} (${v.changeType ?? 'initial'})`);
}

diff(type, name, fromVersion, toVersion, options?)

Compare two versions showing changes. The response shape depends on the format option:

// Section-level summary (default)
const summary = await client.versions.diff('agent', 'code-validator', '1.0.0', '2.0.0');
console.log(summary.sectionsAdded, summary.sectionsRemoved, summary.sectionsModified);

// Unified text diff
const unified = await client.versions.diff('agent', 'code-validator', '1.0.0', '2.0.0', {
  format: 'unified',
});
console.log(unified.unified); // string containing unified diff

// Field-level diff with suggested semver bump
const fields = await client.versions.diff('agent', 'code-validator', '1.0.0', '2.0.0', {
  format: 'fields',
});
console.log(fields.suggestedBump); // 'major' | 'minor' | 'patch'

// Full raw YAML
const full = await client.versions.diff('agent', 'code-validator', '1.0.0', '2.0.0', {
  full: true,
});
console.log(full.fromYaml, full.toYaml);

Validation (client.validation)

Validate definition YAML before creating.

validate(type, yaml)

Validate YAML against the schema.

const result = await client.validation.validate('agent', yamlContent);
if (result.valid) {
  console.log('YAML is valid');
} else {
  console.log('Errors:', result.errors);
}

Dependencies (client.dependencies)

Query dependency relationships between definitions.

v0.31.0 — shape change (live-tests T2 §3.5, R12): dependencies.get() and dependencies.getDependents() now return real envelopes. Prior versions exported DependencyGraph ({nodes?, edges?, cycleDetected?, cycles?}) — every field was optional, so the schema silently parsed every real API response as {}. The types DependencyGraph and DependencyEdge have been removed; the new shapes are DependencyGraphResponse (recursive graph + flat list) and DependentsResponse. Consumers who accessed .nodes / .edges / .cycleDetected need to migrate to .graph.dependencies / .flat / (cycle-detect client-side if needed). In practice every pre-fix consumer was reading {} so real-world breakage is small. See the CHANGELOG for the full migration story.

get(type, name, version, options?)

Get the dependency graph for a definition. Returns DependencyGraphResponse: {definition, graph: DependencyNode (recursive), flat: FlatDep[], totalCount, maxDepth}.

const result = await client.dependencies.get('workflow', 'my-workflow', '1.0.0', {
  maxDepth: 3,
});

// result.graph is the root DependencyNode (recursive tree)
console.log('Root:', result.graph.name, result.graph.version);
console.log('Direct deps:', result.graph.dependencies.length);

// result.flat is a pre-flattened list with depth labels for table rendering
for (const dep of result.flat) {
  console.log(`  depth ${dep.depth}: ${dep.type}/${dep.name}@${dep.version}`);
}

console.log('Total transitive deps:', result.totalCount);
console.log('Max depth resolved:', result.maxDepth);

// Or walk the recursive tree directly:
function walk(node: DependencyNode, depth = 0) {
  const ctx = node.context ? `  [${node.context}]` : '';
  console.log('  '.repeat(depth) + `${node.type}/${node.name}@${node.version}${ctx}`);
  for (const child of node.dependencies) walk(child, depth + 1);
}
walk(result.graph);

getDependents(type, name, version)

Get definitions that depend on this one. Returns DependentsResponse: {definition, dependents: Dependent[], totalCount}. Each Dependent carries a context field describing how it references the target (e.g. "invokes.agent", "phase validate", "dependencies.requires").

const result = await client.dependencies.getDependents('agent', 'code-validator', '1.0.0');

console.log(`${result.totalCount} dependents of`,
  `${result.definition.type}/${result.definition.name}@${result.definition.version}`);

for (const dep of result.dependents) {
  // dep.context describes how the dependent uses this definition
  console.log(`  ${dep.type}/${dep.name}@${dep.version}  ←  ${dep.context}`);
}

Forks (client.forks)

Fork definitions to create derivatives.

create(type, name, version, body)

Fork a definition to a new name.

const forked = await client.forks.create('agent', 'code-validator', '1.0.0', {
  name: 'my-code-validator',
});

isForkable(type, name, version)

Check if a definition can be forked.

const check = await client.forks.isForkable('agent', 'code-validator', '1.0.0');
if (check.canFork) {
  console.log('Can fork!');
}

getAncestry(type, name, version)

Get the fork lineage for a definition. Returns { isFork, fork, source, sourceAvailable } — if the definition is a fork, fork is the fork record and source is a slim summary of the live source definition.

source becomes null (and sourceAvailable becomes false) once the source is deleted. The origin is still readable from the durable snapshot on the fork record — fork.sourceType / fork.sourceName / fork.sourceVersion — captured at fork time and surviving source deletion (registry API ≥ V1 2026-06-16).

const lineage = await client.forks.getAncestry('agent', 'my-validator', '1.0.0');
if (lineage.isFork) {
  if (lineage.sourceAvailable) {
    console.log('Forked from (live):', lineage.source?.name);
  } else {
    // Source deleted — origin still known from the durable snapshot.
    console.log('Forked from (origin):', `${lineage.fork?.sourceName}@${lineage.fork?.sourceVersion}`);
  }
  console.log('Forked at:', lineage.fork?.forkedAt);
}

list(type, name, version)

List all forks of a definition.

const result = await client.forks.list('agent', 'code-validator', '1.0.0');
console.log(result.totalForks); // 2
result.forks.forEach(({ fork, definition }) => {
  console.log(definition?.name, fork.forkedAt);
});

Executions (client.executions)

Record executions and query aggregated statistics.

record(type, name, version, body)

Record an execution of a definition — the write side that feeds execution analytics. Idempotent: if body.runId is provided and already recorded, the existing count is returned (duplicate: true) without double-counting.

| body field | Type | Description | |--------------|------|-------------| | source | string | Attribution label — 'cli', 'api', 'sdk', 'mcp', or any custom string | | runId | string? | Optional idempotency key; repeat calls with the same runId are deduplicated |

const result = await client.executions.record('agent', 'code-validator', '1.0.0', {
  source: 'sdk',
  runId: 'run-abc-123',
});
console.log(result.recorded, result.duplicate, result.executionCount);

Returns RecordExecutionResult: { recorded, duplicate, executionCount, definition }.

getStats(type, name, version, window?)

Get aggregated execution statistics. No authentication required.

const stats = await client.executions.getStats('agent', 'code-validator', '1.0.0', 60);
console.log(`Total executions: ${stats.totalCount}`);
console.log(`Recent executions: ${stats.recentCount}`);
console.log(`Window: ${stats.windowMinutes} minutes`);

Stars (client.stars)

Star and unstar definitions. Stars are tracked per-user per-definition (not per-version). All operations are idempotent and require authentication.

getStatus(type, name, version?)

Check if the authenticated user has starred a definition.

const status = await client.stars.getStatus('agent', 'code-validator');
console.log(status.starred); // true
console.log(status.starCount); // 42

star(type, name, version?)

Star a definition. No-op if already starred.

const result = await client.stars.star('agent', 'code-validator');
console.log(result.starCount); // 43

unstar(type, name, version?)

Unstar a definition. No-op if not starred.

const result = await client.stars.unstar('agent', 'code-validator');
console.log(result.starCount); // 42

Translation (client.translation)

Manage definition translation between schema versions.

getVersion()

Get the current translator version.

const version = await client.translation.getVersion();
console.log(`Translator: ${version.translatorVersion}`);

retranslate(type, name, version, options?)

Re-translate a definition with the latest translator.

const def = await client.translation.retranslate('agent', 'my-agent', '1.0.0', {
  createNewVersion: true,
});

upgradeDefinition(type, name, body)

Upgrade a legacy definition to the current format.

const result = await client.translation.upgradeDefinition('agent', 'legacy-agent', {
  yaml: oldFormatYaml,
});

Models (client.models)

Query the AI model catalog.

list(query?)

List available AI models.

const result = await client.models.list({
  provider: 'anthropic',
  tier: 'premium',
});
for (const model of result.models) {
  // `limits.context` is the model's max context window (tokens). Optional:
  // treat a missing window or `0` as "unknown" — some synced rows have no limit.
  const window = model.limits?.context || undefined;
  console.log(`${model.provider}/${model.modelId}: ${model.displayName} (window: ${window ?? 'unknown'})`);
}

get(provider, modelId)

Get details for a specific model.

const model = await client.models.get('anthropic', 'claude-3-opus');
console.log(model.capabilities);
console.log(model.limits); // { context: 200000, output: 4096 }
console.log(model.cost);   // { input: 3, output: 15, cacheRead: 0.3, ... } — USD per MILLION tokens

Pricing (model.cost) — present on models from list(), get(), and resolveAlias() (registry API 2026-07-26+). Rates are USD per million tokens (models.dev convention). null or absent means unpriced — the registry only emits the block when both input and output rates exist; it never fabricates {input: 0, output: 0}, so a cost of 0 never means "unknown". sourceUpdatedAt is the models.dev per-model last_updated date (model-entry provenance, not a cost-specific capture date) — treat rates without it as provenance-unknown.

if (model.cost) {
  const usd = (inputTokens * model.cost.input + outputTokens * model.cost.output) / 1e6;
}

listProviders()

List all model providers.

const providers = await client.models.listProviders();

listAliases()

List model aliases (e.g., 'latest', 'opus').

const aliases = await client.models.listAliases();

resolveAlias(alias)

Resolve an alias to a concrete model.

const resolution = await client.models.resolveAlias('opus');
console.log(`${resolution.alias} → ${resolution.target}`);

Languages (client.languages)

Access definition language schemas (ADL, CDL, WDL, PDL).

list()

List all definition languages with current version info.

const result = await client.languages.list();
for (const lang of result.languages) {
  console.log(`${lang.abbreviation} v${lang.currentVersion}: ${lang.displayName}`);
}

get(id)

Get a definition language with its full JSON Schema.

const adl = await client.languages.get('adl');
console.log(adl.schema.title);    // "Agent Definition Language (ADL) Schema"
console.log(adl.schema.version);  // "1.16.0"
// adl.schema.content contains the full JSON Schema object

Users (client.users)

Query public user profiles.

get(id)

Get a public user profile by ID.

const user = await client.users.get('user-uuid');
console.log(user.username, user.name);

batch(ids)

Batch lookup multiple users (max 100).

const users = await client.users.batch(['id1', 'id2', 'id3']);
console.log(users['id1']?.username);
// Unknown IDs return null — the key is present but the value is null
console.log(users['nonexistent-id']); // null

Render (client.render)

Get rendered definition output.

get(type, name, version, options?)

Get the fully rendered/resolved definition. Pass "latest" as the version to resolve to the most recent published version.

// Get latest published version
const rendered = await client.render.get('agent', 'code-validator', 'latest');
console.log(rendered.markdown);

// Get specific version
const specific = await client.render.get('agent', 'code-validator', '1.5.0');

// With render profile (optional: 'core' or 'uluops-full')
const full = await client.render.get('agent', 'code-validator', 'latest', {
  renderProfile: 'uluops-full',
});

// Multi-target render (for OpenCode, Codex, Gemini adapters)
const adapted = await client.render.get('agent', 'code-validator', 'latest', {
  target: 'opencode',    // Target harness format
  model: 'gpt-5.3',      // Model override for target envelope
});

preview(type, body)

Preview render without saving.

const preview = await client.render.preview('agent', {
  yaml: rawYaml,
});

Analytics (client.analytics)

Definition effectiveness, health grades, lineage, evolution, and cross-version comparison. Health scores are provisional pending a 90-day calibration study.

getEffectiveness(type, name, version?)

Get effectiveness metrics: pass rate, scores, taxonomy distribution, health score, and composition lift.

Quality numbers are voter-weighted (registry-api >= 0.52): one actor, one vote — a single account cannot dominate them with run volume. metrics.provenance reports who stands behind them (actorCount/voterCount, a provisional/established confidence label, and the independent vs selfReported split — provenance.independent is the headline figure to quote; selfReported is the author rating their own definition). For agents, quality is participation-based (snapshot scores across every run the agent appears in) and effectiveness.passRate is null by design — a run-level gate result cannot be attributed to one constituent. uniqueUsers is the all-time distinct-actor count; provenance counts are windowed.

const eff = await client.analytics.getEffectiveness('agent', 'code-validator');
console.log(eff.metrics.healthScore); // 67
console.log(eff.metrics.provenance?.independent?.runAvgScore); // 90.1 — the headline
console.log(eff.metrics.provenance?.confidence); // 'provisional' until 3+ qualifying actors
console.log(eff.metrics.effectiveness?.passRate); // null for agents (score-only quality)

// Specific version
const v2 = await client.analytics.getEffectiveness('agent', 'code-validator', '2.0.0');

getHealth(type, name, version?)

Get health grade (A-F) and issue profile with failure domain distribution.

const health = await client.analytics.getHealth('agent', 'code-validator');
console.log(health.grade); // 'B'
console.log(health.provisional); // true — weights unvalidated
console.log(health.caveats); // ['PROVISIONAL: ...']

getEcosystemOverview()

Get ecosystem-wide overview: definition counts, aggregate health, top performers, needs-attention list.

const overview = await client.analytics.getEcosystemOverview();
console.log(overview.definitions.total); // 42
console.log(overview.effectiveness.topPerformers);

getLineage(type, name)

Get the lineage graph: versions and forks as a tree with per-node health scores.

const lineage = await client.analytics.getLineage('agent', 'code-validator');
console.log(lineage.totalVersions); // 3
console.log(lineage.totalForks); // 1

getEvolution(type, name)

Get version-over-version metrics timeline with trend detection.

const evo = await client.analytics.getEvolution('agent', 'code-validator');
console.log(evo.trend); // 'improving'
console.log(evo.trendConfidence); // 'high'

getTranslation(type, name)

Get versions grouped by translator version with aggregate metrics.

const translation = await client.analytics.getTranslation('agent', 'code-validator');
for (const group of translation.groups) {
  console.log(`${group.translatorVersion}: ${group.aggregateMetrics.avgPassRate}%`);
}

compare(type, name, versions)

Compare effectiveness across 2-5 definition versions side-by-side.

const cmp = await client.analytics.compare('agent', 'code-validator', ['1.0.0', '1.1.0', '1.2.0']);
for (const v of cmp.versions) {
  console.log(`${v.version}: pass=${v.passRate}%, health=${v.healthScore}`);
}

getDiffImpact(type, name, fromVersion, toVersion)

Get structural diff combined with metric deltas between two versions. Deltas are observational, not causal.

const impact = await client.analytics.getDiffImpact('agent', 'code-validator', '1.0.0', '1.1.0');
console.log(impact.deltas.passRateDelta); // 15
console.log(impact.caveats); // ['OBSERVATIONAL: ...']

Environment Variables

These variables are read by createClientFromEnvironment() and loadConfig() from the /config sub-path. The new RegistryClient() constructor does not read environment variables — pass config explicitly instead.

| Variable | Description | Default | |----------|-------------|---------| | ULUOPS_API_KEY | API key for authentication | - | | ULUOPS_SESSION_TOKEN | JWT session token | - | | ULUOPS_EMAIL | Email for session-based auth | - | | ULUOPS_PASSWORD | Password for session-based auth | - | | ULUOPS_ORG_SLUG | Organization slug for multi-tenancy | - | | ULUOPS_DEBUG | Enable debug logging | false |

Create a .env file in your project:

ULUOPS_API_KEY=ulr_your-api-key-here

Constants

The /config sub-path exports constants for pre-flight checks, debugging, and defensive programming. For browser environments, use the /config/constants sub-path which has no Node.js dependencies:

import {
  MAX_YAML_SIZE,
  SDK_VERSION,
  USER_AGENT,
  HTTP_STATUS,
  ERROR_CODES,
  RETRYABLE_STATUS_CODES,
  ENV_VARS,
  CONFIG_PATHS,
  DEFAULT_BASE_URL,
  DEFAULT_TIMEOUT,
  DEFAULT_RETRY_COUNT,
  API_KEY_PREFIX,
} from '@uluops/registry-sdk/config';

// Validate payload size before uploading
if (yamlBuffer.byteLength > MAX_YAML_SIZE) {
  throw new Error(`YAML exceeds ${MAX_YAML_SIZE} bytes`);
}

// Log the SDK version for debugging
console.log('SDK version:', SDK_VERSION);

// Check error codes programmatically
if (error.code === ERROR_CODES.NOT_FOUND) { /* ... */ }

The sub-path also exports Node.js-only helpers for credential discovery:

import {
  createClientFromEnvironment,
  loadCredentials,
  loadConfig,
  loadStoredCredentials,
  loadEnvFiles,
  getGlobalConfigDir,
  getCredentialsPath,
  isApiKey,
  validateCredentials,
} from '@uluops/registry-sdk/config';

Error Handling

The SDK provides a typed error hierarchy so you can catch and recover from specific failure modes.

Error Classes

| Error | Status | When It Happens | |-------|--------|-----------------| | ValidationError | 400 | Malformed request — invalid params, missing fields | | UnauthorizedError | 401 | No credentials, expired token, invalid API key | | ForbiddenError | 403 | Valid credentials but insufficient permissions or subscription tier | | NotFoundError | 404 | Definition, model, or user doesn't exist | | ConflictError | 409 | Name collision, publishing already-published definition | | PayloadTooLargeError | 413 | YAML exceeds 150KB limit | | UnprocessableError | 422 | Valid YAML syntax but invalid semantics (missing refs, cycles) | | RateLimitError | 429 | Too many requests (100 executions/min per definition) | | ServiceUnavailableError | 503 | Server temporarily down or overloaded | | NetworkError | - | DNS failure, connection refused, network unreachable (auto-retried) | | TimeoutError | - | Request exceeded timeout (default: 30s) | | ResponseValidationError | 0 | API response did not match the SDK's expected Zod schema (contract drift). Extends RegistryApiError; original ZodError preserved on .zodError. Non-retryable. |

All API errors extend RegistryApiError and include:

  • statusCode — HTTP status code (0 for network/timeout/response-validation)
  • code — Machine-readable error code (e.g., 'NOT_FOUND', 'RATE_LIMIT_ERROR')
  • message — Human-readable description
  • details — Optional structured metadata
  • requestId — Server request ID for support/debugging

Basic Error Handling

import {
  RegistryApiError,
  NotFoundError,
  ValidationError,
  isRegistryApiError,
} from '@uluops/registry-sdk/errors';

try {
  const def = await client.definitions.get('agent', 'my-agent', '1.0.0');
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log('Definition not found');
  } else if (error instanceof ValidationError) {
    console.log('Bad request:', error.details);
  } else if (isRegistryApiError(error)) {
    console.log(`API error [${error.code}]: ${error.message}`);
  } else {
    throw error; // Unexpected non-API error
  }
}

Recovery Patterns

Handling Authentication Failures

import { UnauthorizedError, ForbiddenError } from '@uluops/registry-sdk/errors';

try {
  await client.definitions.create('agent', 'my-agent', { yaml });
} catch (error) {
  if (error instanceof UnauthorizedError) {
    // Token expired or invalid — re-authenticate
    const newToken = await refreshMyToken();
    const retryClient = new RegistryClient({ sessionToken: newToken });
    await retryClient.definitions.create('agent', 'my-agent', { yaml });
  } else if (error instanceof ForbiddenError) {
    // Valid auth but wrong role/tier — can't retry, need elevated permissions
    console.error('Requires publisher role or pro subscription');
  }
}

Rate Limit Backoff

The SDK auto-retries on 429 with exponential backoff, but if all retries are exhausted:

import { RateLimitError } from '@uluops/registry-sdk/errors';

try {
  await client.executions.record('agent', 'my-agent', '1.0.0', { source: 'cli' });
} catch (error) {
  if (error instanceof RateLimitError) {
    const waitMs = (error.retryAfter ?? 60) * 1000;
    console.log(`Rate limited. Waiting ${waitMs / 1000}s...`);
    await new Promise((r) => setTimeout(r, waitMs));
    await client.executions.record('agent', 'my-agent', '1.0.0', { source: 'cli' });
  }
}

Handling YAML Validation Errors

Two distinct failure modes for YAML — catch them separately:

import { ValidationError, UnprocessableError, PayloadTooLargeError } from '@uluops/registry-sdk/errors';

try {
  await client.definitions.create('agent', 'my-agent', { yaml: rawYaml });
} catch (error) {
  if (error instanceof PayloadTooLargeError) {
    // YAML > 150KB — split or compress before retrying
    console.error('YAML too large. Max: 150KB');
  } else if (error instanceof ValidationError) {
    // Malformed request (e.g., missing required fields in body)
    console.error('Request validation failed:', error.details);
  } else if (error instanceof UnprocessableError) {
    // YAML parses but is semantically invalid (bad refs, missing interface, cycles)
    console.error('YAML semantic errors:', error.details);
  }
}

Conflict Resolution

Conflicts arise from name collisions or state transitions:

import { ConflictError } from '@uluops/registry-sdk/errors';

try {
  await client.definitions.create('agent', 'my-agent', { yaml });
} catch (error) {
  if (error instanceof ConflictError) {
    // Name already taken — try updating instead, or choose a different name
    console.log('Definition already exists, updating...');
    await client.definitions.update('agent', 'my-agent', '1.0.0', { yaml });
  }
}

Network Resilience

For unreliable networks, combine timeout config with manual retry:

import { NetworkError, TimeoutError, ServiceUnavailableError } from '@uluops/registry-sdk/errors';

async function resilientFetch() {
  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await client.definitions.list({ type: 'agent', limit: 10 });
    } catch (error) {
      const isTransient =
        error instanceof NetworkError ||
        error instanceof TimeoutError ||
        error instanceof ServiceUnavailableError;

      if (isTransient && attempt < maxAttempts) {
        const delay = 1000 * Math.pow(2, attempt - 1);
        console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
      throw error;
    }
  }
}

Logging All Errors with Request ID

For production debugging, log the requestId so support can trace the request server-side:

import { isRegistryApiError } from '@uluops/registry-sdk/errors';

try {
  await client.definitions.publish('agent', 'my-agent', '1.0.0');
} catch (error) {
  if (isRegistryApiError(error)) {
    console.error(JSON.stringify({
      level: 'error',
      code: error.code,
      status: error.statusCode,
      message: error.message,
      requestId: error.requestId,
      details: error.details,
    }));
  }
}

Automatic Retries

The SDK automatically retries GET requests on transient errors (502, 503, 504, 429) with exponential backoff and jitter. Idempotent mutations (publish, deprecate, archive, record execution, star/unstar) are also retried automatically. Non-idempotent mutations (create, update, delete) are not retried by default to prevent duplicate side effects.

const client = new RegistryClient({
  apiKey: process.env.ULUOPS_API_KEY,
  retries: 3,        // Max retry attempts (default: 3)
  timeout: 30000,    // Request timeout in ms (default: 30000)
});

Retryable errors: 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout, 429 Too Many Requests, and NetworkError (DNS failures, connection resets, ECONNREFUSED).

Advanced Usage

Server-Side Normalization

UDL definition languages (CDL, WDL, PDL) use an ergonomic authoring format that differs from the runtime structure executors expect. Pass normalize: true to get the runtime-ready shape computed server-side:

const def = await client.definitions.get('command', 'code-validate', '1.0.0', {
  normalize: true,
});

// def.normalized contains the runtime-ready definition object
// def.normalizationError is set if normalization failed (def.normalized will be null)

The normalized field is Zod-validated as part of the standard response schema. Normalization defaults to false — existing consumers are unaffected.

Migration from v0.25.0: The @uluops/registry-sdk/normalization subpath has been removed. Normalization now lives server-side in the API (powered by @uluops/definition-factory). For offline normalization, import directly from @uluops/definition-factory.

See also: ADR-003 for design rationale and migration history.

Auth Strategies

The SDK exports ApiKeyAuth, JwtSessionAuth, and createAuthStrategy for advanced composition. Most users should use RegistryClient directly — these are for cases where you need to manage auth independently of the client (e.g., sharing a token across multiple SDK instances or implementing custom refresh logic).

import { ApiKeyAuth, JwtSessionAuth, createAuthStrategy } from '@uluops/registry-sdk';

// Auto-detect from config
const auth = createAuthStrategy({ apiKey: process.env.ULUOPS_API_KEY });

// Or construct directly
const apiAuth = new ApiKeyAuth(process.env.ULUOPS_API_KEY!);

Using the Low-Level HTTP Client

Access the HTTP client from an existing RegistryClient via getHttpClient(), or construct one directly:

// From an existing client (preserves auth config)
const http = client.getHttpClient();
const data = await http.get<MyType>('/custom/endpoint');

// Or construct directly
import { RegistryHttpClient } from '@uluops/registry-sdk';

const http = new RegistryHttpClient({
  apiKey: process.env.ULUOPS_API_KEY,
});

// Make raw requests
const data = await http.get<MyType>('/custom/endpoint', { param: 'value' });
const result = await http.post<MyType>('/custom/endpoint', { body: 'data' });

Node.js Environment Discovery

Use createClientFromEnvironment() to auto-discover credentials from environment variables, .env files, and ~/.uluops/credentials.json:

import { createClientFromEnvironment } from '@uluops/registry-sdk/config';

// Auto-discover all config from environment
const client = createClientFromEnvironment();

// Auto-discover with overrides
const clientWithOverrides = createClientFromEnvironment({
  debug: true,
});

You can also load config manually:

import { loadCredentials, loadConfig } from '@uluops/registry-sdk/config';

// Load from environment and config files
const credentials = loadCredentials();
console.log(credentials.apiKey);

// Load full config
const config = loadConfig();
console.log(config.apiKey);

Note: The /config sub-path uses Node.js built-ins (node:fs, node:path, node:os) and cannot be imported in browser environments.

Browser Usage

The main SDK entry point (@uluops/registry-sdk) is browser-safe. The RegistryClient constructor and all operation methods use fetch internally — no Node.js built-ins are required.

// Works in Next.js, React, Vite, or any browser bundler
import { RegistryClient } from '@uluops/registry-sdk';

// In browser apps, inject the key from your backend — never bundle it in client code
const client = new RegistryClient({
  apiKey: apiKeyFromServer,
});

const models = await client.models.list({ provider: 'anthropic' });

Type-only imports are also browser-safe:

import type { Model, ModelAlias, Provider } from '@uluops/registry-sdk/types';

Do not import @uluops/registry-sdk/config in browser code — it uses node:fs for reading credential files and .env loading.

CORS: The SDK is browser-safe (native fetch, no Node.js APIs), but the Registry API does not serve CORS headers by default. Browser requests to the API will fail with opaque CORS errors unless you proxy them through your own backend. Recommended patterns:

  • Next.js API routesapp/api/registry/route.ts proxies to the Registry API server-side
  • Express/Fastify middleware — forward /api/registry/* to the upstream API
  • Reverse proxy (nginx/Caddy) — add Access-Control-Allow-Origin at the edge

Do not configure the API to return Access-Control-Allow-Origin: * with authenticated requests — this exposes API keys to any origin. Use an allowlist or proxy instead.

License

MIT License - see LICENSE for details.