@uluops/registry-sdk
v0.30.2
Published
SDK for the UluOps Registry API - manage AI workflow definitions
Maintainers
Readme
UluOps · Operating Intelligence as Infrastructure
@uluops/registry-sdk
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 withtscfirst. 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-hereimport { 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
- Installation
- Authentication
- TypeScript Support
- API Reference
- Environment Variables
- Error Handling
- Advanced Usage
- License
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-sdkRequirements:
- Node.js 18.0.0 or higher (server-side)
- Any modern browser with
fetchsupport (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 afterlogin()succeeds. Any API call made before login will fail withUnauthorizedError.
// 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:
- Explicit arguments:
apiKey,sessionTokenpassed to the function - Environment variables:
ULUOPS_API_KEY,ULUOPS_SESSION_TOKEN - Local
.envfile: In the current working directory - 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. UsecreateClientFromEnvironment()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,
ValidationFieldError,
Model,
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)
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}`);
},
});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', 'executionCount', etc.) |
| 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,
});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.
const def = await client.definitions.publish('agent', 'my-agent', '1.0.0');
console.log(def.status); // 'published'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');Versions (client.versions)
Manage definition version history.
list(type, name, options?)
List all versions of a definition.
const { versions } = await client.versions.list('agent', 'code-validator', {
limit: 20,
offset: 0,
});
for (const v of versions) {
console.log(`${v.version}: ${v.status}`);
}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.
get(type, name, version, options?)
Get the dependency graph for a definition.
const graph = await client.dependencies.get('workflow', 'my-workflow', '1.0.0', {
maxDepth: 3,
});
console.log('Nodes:', graph.nodes);
console.log('Edges:', graph.edges);
console.log('Cycles detected:', graph.cycleDetected);getDependents(type, name, version)
Get definitions that depend on this one.
const dependents = await client.dependencies.getDependents('agent', 'code-validator', '1.0.0');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 } — if the definition is a fork, fork is the fork record and source is a slim summary of the source definition.
const lineage = await client.forks.getAncestry('agent', 'my-validator', '1.0.0');
if (lineage.isFork) {
console.log('Forked from:', lineage.source?.name);
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)
Query execution statistics.
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); // 42star(type, name, version?)
Star a definition. No-op if already starred.
const result = await client.stars.star('agent', 'code-validator');
console.log(result.starCount); // 43unstar(type, name, version?)
Unstar a definition. No-op if not starred.
const result = await client.stars.unstar('agent', 'code-validator');
console.log(result.starCount); // 42Translation (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) {
console.log(`${model.provider}/${model.modelId}: ${model.displayName}`);
}get(provider, modelId)
Get details for a specific model.
const model = await client.models.get('anthropic', 'claude-3-opus');
console.log(model.capabilities);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 objectUsers (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']); // nullRender (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.
const eff = await client.analytics.getEffectiveness('agent', 'code-validator');
console.log(eff.metrics.healthScore); // 67
console.log(eff.metrics.effectiveness?.passRate); // 49.4
// 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); // 1getEvolution(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-hereConstants
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 | * | API response did not match expected Zod schema (from @uluops/registry-sdk/errors) |
All API errors extend RegistryApiError and include:
statusCode— HTTP status code (0 for network/timeout)code— Machine-readable error code (e.g.,'NOT_FOUND','RATE_LIMIT_ERROR')message— Human-readable descriptiondetails— Optional structured metadatarequestId— 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/normalizationsubpath 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
/configsub-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 routes —
app/api/registry/route.tsproxies to the Registry API server-side- Express/Fastify middleware — forward
/api/registry/*to the upstream API- Reverse proxy (nginx/Caddy) — add
Access-Control-Allow-Originat the edgeDo 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.
