@uluops/registry-sdk
v0.47.1
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,
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
isVerdictTrustworthybefore trusting the verdict. A profile whose sync scan failed to complete still carriesaggregateRiskLevel: '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. ReadingaggregateRiskLeveldirectly would render either crash as safe. The exportedisVerdictTrustworthy(profile)predicate returnsfalsefor 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 DefinitionListItem — riskLevel, 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()anddependencies.getDependents()now return real envelopes. Prior versions exportedDependencyGraph({nodes?, edges?, cycleDetected?, cycles?}) — every field was optional, so the schema silently parsed every real API response as{}. The typesDependencyGraphandDependencyEdgehave been removed; the new shapes areDependencyGraphResponse(recursivegraph+ flat list) andDependentsResponse. Consumers who accessed.nodes/.edges/.cycleDetectedneed 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); // 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) {
// `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 tokensPricing (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 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.
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); // 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 | 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 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.
