30 KiB
Phase 6F: UBA Community
CLI Tools, Schema Gallery, and Community Resources
Phase: 6F of 6
Duration: 2 weeks (10 working days)
Priority: MEDIUM
Status: NOT STARTED
Depends On: Phase 6D, 6E complete
Overview
Phase 6F focuses on community enablement: tools that help backend developers create and validate UBA schemas, a gallery for discovering community backends, and resources that foster ecosystem growth.
This phase is about making UBA accessible and encouraging adoption beyond Visual Hive's own needs.
Community Vision
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Developer creates Schema validated Community uses │
│ a cool backend ───► and published ───► backend easily │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Python/Go/ │ │ npx nodegx │ │ Browse in │ │
│ │ Node backend │ ───► │ validate │ ───► │ Nodegx │ │
│ │ + schema.yaml│ │ publish │ │ gallery │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Goals
- Schema validation CLI - Help developers validate schemas before deploying
- Schema gallery - Browse and discover community backends (future)
- Submission process - How to contribute backends to the gallery
- Community resources - Templates, examples, Discord/forum
Prerequisites
- Phase 6D complete ✅ (documentation exists)
- Phase 6E complete ✅ (reference implementation exists)
Task Breakdown
UBA-029: Schema Validation CLI
Effort: 2 days
Assignee: TBD
Branch: feature/uba-029-validation-cli
Description
Create a CLI tool that validates UBA schemas, tests backend endpoints, and provides helpful feedback for developers.
Features
- Validate schema file - Check YAML syntax and schema compliance
- Test live backend - Fetch schema, test endpoints
- Generate types - Output TypeScript types from schema
- Init command - Create starter schema file
Usage
# Install globally
npm install -g @nodegx/uba-cli
# Or use npx
npx @nodegx/uba-cli validate ./schema.yaml
# Commands
nodegx-uba validate <file> # Validate a local schema file
nodegx-uba test <url> # Test a live backend
nodegx-uba init # Create a starter schema
nodegx-uba types <file> -o types.ts # Generate TypeScript types
Files to Create
packages/nodegx-uba-cli/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│ ├── index.ts # CLI entry point
│ ├── commands/
│ │ ├── validate.ts
│ │ ├── test.ts
│ │ ├── init.ts
│ │ └── types.ts
│ ├── validator/
│ │ ├── schema.ts
│ │ └── endpoints.ts
│ ├── generator/
│ │ └── typescript.ts
│ └── templates/
│ └── starter-schema.yaml
└── tests/
└── ...
Implementation
// src/index.ts
#!/usr/bin/env node
import { Command } from 'commander';
import { validateCommand } from './commands/validate';
import { testCommand } from './commands/test';
import { initCommand } from './commands/init';
import { typesCommand } from './commands/types';
import { version } from '../package.json';
const program = new Command();
program
.name('nodegx-uba')
.description('Nodegx Universal Backend Adapter CLI')
.version(version);
program
.command('validate <file>')
.description('Validate a UBA schema file')
.option('-s, --strict', 'Enable strict validation')
.option('-q, --quiet', 'Only output errors')
.action(validateCommand);
program
.command('test <url>')
.description('Test a live UBA backend')
.option('-t, --token <token>', 'Auth token')
.option('--skip-config', 'Skip config endpoint test')
.option('--skip-health', 'Skip health endpoint test')
.action(testCommand);
program
.command('init')
.description('Create a starter schema file')
.option('-o, --output <file>', 'Output file', 'nodegx-schema.yaml')
.option('--advanced', 'Include all optional fields')
.action(initCommand);
program
.command('types <file>')
.description('Generate TypeScript types from schema')
.option('-o, --output <file>', 'Output file', 'uba-types.ts')
.action(typesCommand);
program.parse();
// src/commands/validate.ts
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import Ajv from 'ajv';
import chalk from 'chalk';
import { schemaV1 } from '../validator/schema';
interface ValidateOptions {
strict?: boolean;
quiet?: boolean;
}
export async function validateCommand(file: string, options: ValidateOptions) {
console.log(chalk.blue(`\nValidating ${file}...\n`));
// Check file exists
if (!fs.existsSync(file)) {
console.error(chalk.red(`Error: File not found: ${file}`));
process.exit(1);
}
// Read and parse YAML
let schema: any;
try {
const content = fs.readFileSync(file, 'utf-8');
schema = yaml.load(content);
} catch (error) {
if (error instanceof yaml.YAMLException) {
console.error(chalk.red(`YAML Parse Error (line ${error.mark?.line}):`));
console.error(chalk.red(` ${error.message}`));
process.exit(1);
}
throw error;
}
// Validate against JSON Schema
const ajv = new Ajv({ allErrors: true, verbose: true });
const validate = ajv.compile(schemaV1);
const valid = validate(schema);
if (!valid) {
console.error(chalk.red('Schema Validation Failed:\n'));
for (const error of validate.errors || []) {
const path = error.instancePath || '/';
const message = formatError(error);
console.error(chalk.red(` ✗ ${path}: ${message}`));
}
console.error(chalk.red(`\n${validate.errors?.length} error(s) found.`));
process.exit(1);
}
// Additional validations
const warnings = runAdditionalValidations(schema, options.strict);
if (warnings.length > 0 && !options.quiet) {
console.log(chalk.yellow('Warnings:\n'));
for (const warning of warnings) {
console.log(chalk.yellow(` ⚠ ${warning}`));
}
console.log();
}
// Success
console.log(chalk.green('✓ Schema is valid!\n'));
// Print summary
if (!options.quiet) {
printSchemaSummary(schema);
}
}
function formatError(error: any): string {
switch (error.keyword) {
case 'required':
return `Missing required field: ${error.params.missingProperty}`;
case 'enum':
return `Invalid value. Expected one of: ${error.params.allowedValues.join(', ')}`;
case 'type':
return `Expected ${error.params.type}`;
case 'additionalProperties':
return `Unknown field: ${error.params.additionalProperty}`;
default:
return error.message || 'Validation error';
}
}
function runAdditionalValidations(schema: any, strict?: boolean): string[] {
const warnings: string[] = [];
// Check for missing optional but recommended fields
if (!schema.backend.description) {
warnings.push('backend.description is recommended');
}
if (!schema.backend.homepage) {
warnings.push('backend.homepage is recommended for documentation');
}
// Check for empty sections
for (const section of schema.sections) {
if (section.fields.length === 0) {
warnings.push(`Section "${section.id}" has no fields`);
}
}
// Check for duplicate IDs
const sectionIds = new Set<string>();
for (const section of schema.sections) {
if (sectionIds.has(section.id)) {
warnings.push(`Duplicate section ID: ${section.id}`);
}
sectionIds.add(section.id);
const fieldIds = new Set<string>();
for (const field of section.fields) {
if (fieldIds.has(field.id)) {
warnings.push(`Duplicate field ID in ${section.id}: ${field.id}`);
}
fieldIds.add(field.id);
}
}
// Strict mode checks
if (strict) {
if (!schema.backend.endpoints.health) {
warnings.push('[strict] Health endpoint is recommended');
}
// Check for fields without descriptions
for (const section of schema.sections) {
for (const field of section.fields) {
if (!field.description) {
warnings.push(`[strict] Field "${section.id}.${field.id}" has no description`);
}
}
}
}
return warnings;
}
function printSchemaSummary(schema: any) {
console.log(chalk.bold('Schema Summary:'));
console.log(` Name: ${schema.backend.name}`);
console.log(` Version: ${schema.backend.version}`);
console.log(` Sections: ${schema.sections.length}`);
let totalFields = 0;
for (const section of schema.sections) {
totalFields += section.fields.length;
}
console.log(` Total Fields: ${totalFields}`);
console.log(` Debug: ${schema.debug?.enabled ? 'Enabled' : 'Disabled'}`);
console.log();
}
// src/commands/test.ts
import chalk from 'chalk';
import ora from 'ora';
interface TestOptions {
token?: string;
skipConfig?: boolean;
skipHealth?: boolean;
}
export async function testCommand(url: string, options: TestOptions) {
console.log(chalk.blue(`\nTesting backend at ${url}\n`));
const results: TestResult[] = [];
// Test schema endpoint
const schemaSpinner = ora('Fetching schema...').start();
try {
const schemaUrl = new URL('/.well-known/nodegx-schema.yaml', url);
const response = await fetch(schemaUrl.toString());
if (!response.ok) {
schemaSpinner.fail(`Schema endpoint returned ${response.status}`);
results.push({ name: 'Schema', passed: false, error: `HTTP ${response.status}` });
} else {
const content = await response.text();
const schema = yaml.load(content);
schemaSpinner.succeed(`Schema fetched: ${schema.backend.name} v${schema.backend.version}`);
results.push({ name: 'Schema', passed: true });
// Store schema for other tests
globalSchema = schema;
}
} catch (error) {
schemaSpinner.fail(`Schema fetch failed: ${error.message}`);
results.push({ name: 'Schema', passed: false, error: error.message });
}
// Test health endpoint
if (!options.skipHealth && globalSchema?.backend.endpoints.health) {
const healthSpinner = ora('Checking health...').start();
try {
const healthUrl = new URL(globalSchema.backend.endpoints.health, url);
const response = await fetch(healthUrl.toString(), {
headers: buildHeaders(options.token)
});
if (!response.ok) {
healthSpinner.fail(`Health endpoint returned ${response.status}`);
results.push({ name: 'Health', passed: false, error: `HTTP ${response.status}` });
} else {
const data = await response.json();
healthSpinner.succeed(`Health: ${data.status}`);
results.push({ name: 'Health', passed: data.status === 'healthy' });
}
} catch (error) {
healthSpinner.fail(`Health check failed: ${error.message}`);
results.push({ name: 'Health', passed: false, error: error.message });
}
}
// Test config endpoint (with empty config)
if (!options.skipConfig && globalSchema?.backend.endpoints.config) {
const configSpinner = ora('Testing config endpoint...').start();
try {
const configUrl = new URL(globalSchema.backend.endpoints.config, url);
const response = await fetch(configUrl.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...buildHeaders(options.token)
},
body: JSON.stringify({
config: {},
metadata: {
project_id: 'test',
project_name: 'CLI Test',
environment: 'test',
nodegx_version: '1.0.0'
}
})
});
const data = await response.json();
if (data.errors && data.errors.length > 0) {
// Expected - empty config should fail validation
configSpinner.succeed('Config endpoint responding (validation works)');
results.push({ name: 'Config', passed: true });
} else if (data.success) {
configSpinner.warn('Config endpoint accepted empty config (no validation?)');
results.push({ name: 'Config', passed: true, warning: 'No validation' });
}
} catch (error) {
configSpinner.fail(`Config test failed: ${error.message}`);
results.push({ name: 'Config', passed: false, error: error.message });
}
}
// Print summary
console.log(chalk.bold('\nTest Summary:'));
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
for (const result of results) {
if (result.passed) {
console.log(chalk.green(` ✓ ${result.name}`));
} else {
console.log(chalk.red(` ✗ ${result.name}: ${result.error}`));
}
}
console.log();
console.log(`${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
}
// src/commands/init.ts
import * as fs from 'fs';
import chalk from 'chalk';
import inquirer from 'inquirer';
interface InitOptions {
output: string;
advanced?: boolean;
}
export async function initCommand(options: InitOptions) {
console.log(chalk.blue('\nCreate a new UBA schema\n'));
// Interactive prompts
const answers = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Backend ID (lowercase, no spaces):',
default: 'my-backend',
validate: (input) => /^[a-z][a-z0-9-]*$/.test(input) || 'Use lowercase letters, numbers, and hyphens'
},
{
type: 'input',
name: 'name',
message: 'Backend name (display name):',
default: 'My Backend'
},
{
type: 'input',
name: 'description',
message: 'Description:',
default: 'A UBA-compatible backend'
},
{
type: 'input',
name: 'version',
message: 'Version:',
default: '1.0.0'
},
{
type: 'confirm',
name: 'debug',
message: 'Enable debug streaming?',
default: false
}
]);
// Generate schema
const schema = generateSchema(answers, options.advanced);
// Write file
fs.writeFileSync(options.output, schema);
console.log(chalk.green(`\n✓ Created ${options.output}`));
console.log(chalk.gray('\nNext steps:'));
console.log(chalk.gray(' 1. Edit the schema to add your fields'));
console.log(chalk.gray(' 2. Run: nodegx-uba validate ' + options.output));
console.log(chalk.gray(' 3. Serve the schema at /.well-known/nodegx-schema.yaml'));
}
function generateSchema(answers: any, advanced?: boolean): string {
const lines = [
'# Nodegx UBA Schema',
`# Generated by nodegx-uba CLI`,
'',
'schema_version: "1.0"',
'',
'backend:',
` id: "${answers.id}"`,
` name: "${answers.name}"`,
` description: "${answers.description}"`,
` version: "${answers.version}"`,
'',
' endpoints:',
' config: "/nodegx/config"',
' health: "/health"',
];
if (answers.debug) {
lines.push(' debug_stream: "/nodegx/debug"');
lines.push('');
lines.push(' capabilities:');
lines.push(' hot_reload: true');
lines.push(' debug: true');
}
lines.push('');
lines.push('sections:');
lines.push(' - id: "settings"');
lines.push(' name: "Settings"');
lines.push(' description: "Configure your backend"');
lines.push(' fields:');
lines.push(' - id: "api_key"');
lines.push(' type: "secret"');
lines.push(' name: "API Key"');
lines.push(' description: "Your API key"');
lines.push(' required: true');
lines.push('');
lines.push(' - id: "enabled"');
lines.push(' type: "boolean"');
lines.push(' name: "Enabled"');
lines.push(' default: true');
if (answers.debug) {
lines.push('');
lines.push('debug:');
lines.push(' enabled: true');
lines.push(' event_types:');
lines.push(' - id: "request"');
lines.push(' name: "Request"');
lines.push(' fields:');
lines.push(' - id: "message"');
lines.push(' type: "string"');
lines.push(' - id: "duration_ms"');
lines.push(' type: "number"');
lines.push(' format: "duration"');
}
return lines.join('\n');
}
// src/commands/types.ts
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import chalk from 'chalk';
interface TypesOptions {
output: string;
}
export async function typesCommand(file: string, options: TypesOptions) {
console.log(chalk.blue(`\nGenerating TypeScript types from ${file}...\n`));
const content = fs.readFileSync(file, 'utf-8');
const schema = yaml.load(content);
const types = generateTypes(schema);
fs.writeFileSync(options.output, types);
console.log(chalk.green(`✓ Generated ${options.output}`));
}
function generateTypes(schema: any): string {
const lines = [
'// Auto-generated by nodegx-uba CLI',
'// Do not edit manually',
'',
`export interface ${pascalCase(schema.backend.id)}Config {`,
];
for (const section of schema.sections) {
lines.push(` ${section.id}: {`);
for (const field of section.fields) {
const tsType = fieldTypeToTs(field.type);
const optional = field.required ? '' : '?';
lines.push(` ${field.id}${optional}: ${tsType};`);
}
lines.push(' };');
}
lines.push('}');
return lines.join('\n');
}
function fieldTypeToTs(fieldType: string): string {
switch (fieldType) {
case 'string':
case 'text':
case 'secret':
case 'url':
case 'email':
case 'prompt':
case 'code':
case 'color':
return 'string';
case 'number':
case 'slider':
return 'number';
case 'boolean':
return 'boolean';
case 'select':
return 'string';
case 'multi_select':
return 'string[]';
case 'array':
return 'any[]';
case 'object':
case 'field_mapping':
case 'key_value':
return 'Record<string, any>';
case 'tool_toggle':
return '{ enabled: boolean; [key: string]: any }';
default:
return 'any';
}
}
function pascalCase(str: string): string {
return str
.split(/[-_]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
package.json
{
"name": "@nodegx/uba-cli",
"version": "1.0.0",
"description": "CLI tools for Nodegx Universal Backend Adapter",
"main": "dist/index.js",
"bin": {
"nodegx-uba": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"test": "jest"
},
"dependencies": {
"ajv": "^8.12.0",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"inquirer": "^9.2.0",
"js-yaml": "^4.1.0",
"ora": "^7.0.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
Acceptance Criteria
validatecommand workstestcommand worksinitcommand creates valid schematypescommand generates TypeScript- Published to npm
- README documentation
UBA-030: Schema Gallery (Future)
Effort: 5 days
Assignee: TBD
Branch: feature/uba-030-schema-gallery
Status: FUTURE - Stretch goal
Description
Create a gallery for discovering and adding community backends directly from within Nodegx.
Note: This is a stretch goal for Phase 6F. If time permits, implement the basics. Otherwise, document the design for future implementation.
Design
┌─────────────────────────────────────────────────────────────────┐
│ Schema Gallery [Submit Yours]│
├─────────────────────────────────────────────────────────────────┤
│ │
│ Search: [ ] Category: [All Categories ▼] │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Featured │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 🧠 Erleah AI │ │ 📊 Analytics │ │ 🔔 Notifications│ │
│ │ Agent │ │ Engine │ │ Service │ │
│ │ │ │ │ │ │ │
│ │ AI assistant │ │ Event tracking │ │ Push notifs via │ │
│ │ for conferences │ │ and analytics │ │ Firebase/APNS │ │
│ │ │ │ │ │ │ │
│ │ ⭐ 4.8 (234) │ │ ⭐ 4.5 (189) │ │ ⭐ 4.3 (156) │ │
│ │ [Add Backend] │ │ [Add Backend] │ │ [Add Backend] │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ Community │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 💬 Chat Service │ │ 📧 Email Sender │ │ 🗄️ File Storage │ │
│ │ ... │ │ ... │ │ ... │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Gallery Entry Format
# gallery/erleah-ai-agent.yaml
id: "erleah-ai-agent"
name: "Erleah AI Agent"
description: "Agentic AI assistant for conference attendees"
author: "Visual Hive"
author_url: "https://visualhive.co"
version: "1.0.0"
category: "AI/ML"
tags: ["ai", "agent", "conference", "langraph"]
icon: "https://erleah.com/icon.png"
homepage: "https://docs.erleah.com"
repository: "https://github.com/visualhive/erleah"
# How to get the backend running
deployment:
docker:
image: "visualhive/erleah:latest"
compose_url: "https://raw.githubusercontent.com/visualhive/erleah/main/docker-compose.yml"
cloud:
railway: "https://railway.app/template/erleah"
render: "https://render.com/deploy?repo=visualhive/erleah"
# Schema URL (for validation)
schema_url: "https://raw.githubusercontent.com/visualhive/erleah/main/nodegx-schema.yaml"
# Stats (populated by registry)
stats:
installs: 234
rating: 4.8
reviews: 42
API Endpoints (Registry Service)
GET /api/gallery # List all backends
GET /api/gallery/:id # Get backend details
GET /api/gallery/search?q=... # Search backends
GET /api/gallery/categories # List categories
POST /api/gallery/submit # Submit new backend
POST /api/gallery/:id/review # Submit review
Submission Process
- Developer creates GitHub issue using template
- Automated validation runs:
- Schema URL accessible
- Schema validates
- Health endpoint responds
- Docker image builds
- Manual review by maintainers
- Added to gallery with "unverified" badge
- After 10+ installs, eligible for "verified" badge
Files to Create (If Implementing)
packages/noodl-editor/src/editor/src/views/UBA/
├── Gallery/
│ ├── Gallery.tsx
│ ├── Gallery.module.scss
│ ├── GalleryCard.tsx
│ ├── GalleryDetail.tsx
│ └── SubmitDialog.tsx
# Registry service (separate repo)
nodegx-gallery-api/
├── src/
│ ├── routes/gallery.ts
│ ├── services/validation.ts
│ └── models/backend.ts
└── data/
└── backends/
├── erleah-ai-agent.yaml
└── ...
Minimum Viable Gallery
If implementing in Phase 6F, focus on:
- Static gallery - Hardcoded list of 3-5 backends
- Add from gallery - Click to add backend URL
- Basic search - Client-side filtering
- Submit link - Opens GitHub issue template
Save for future:
- Full registry service
- Reviews and ratings
- Docker deployment integration
- Verification system
Acceptance Criteria (MVP)
- Gallery shows hardcoded backends
- Click adds backend to project
- Basic search/filter works
- Submit link works
- Design documented for future
Community Resources
GitHub Repository Structure
nodegx/
├── packages/
│ ├── noodl-editor/ # Main editor
│ ├── nodegx-uba-cli/ # CLI tool
│ └── ...
├── docs/
│ └── uba/ # UBA documentation
├── examples/
│ └── uba-backends/ # Example backend implementations
│ ├── python-fastapi/
│ ├── node-express/
│ └── go-fiber/
└── gallery/ # Gallery entries (for MVP)
├── erleah-ai-agent.yaml
└── ...
Example Backend Templates
Create starter templates for common frameworks:
Python/FastAPI
# examples/uba-backends/python-fastapi/main.py
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel
from typing import Dict, Any
app = FastAPI()
# Serve schema
@app.get("/.well-known/nodegx-schema.yaml")
async def get_schema():
return FileResponse("nodegx-schema.yaml", media_type="application/x-yaml")
# Config endpoint
class ConfigRequest(BaseModel):
config: Dict[str, Any]
metadata: Dict[str, Any]
@app.post("/nodegx/config")
async def apply_config(request: ConfigRequest):
# TODO: Apply configuration
return {
"success": True,
"applied_at": datetime.utcnow().isoformat() + "Z"
}
# Health endpoint
@app.get("/health")
async def health():
return {"status": "healthy", "version": "1.0.0"}
Node.js/Express
// examples/uba-backends/node-express/index.js
const express = require('express');
const fs = require('fs');
const app = express();
app.use(express.json());
// Serve schema
app.get('/.well-known/nodegx-schema.yaml', (req, res) => {
res.type('application/x-yaml');
res.sendFile(__dirname + '/nodegx-schema.yaml');
});
// Config endpoint
app.post('/nodegx/config', (req, res) => {
const { config, metadata } = req.body;
// TODO: Apply configuration
res.json({
success: true,
applied_at: new Date().toISOString()
});
});
// Health endpoint
app.get('/health', (req, res) => {
res.json({ status: 'healthy', version: '1.0.0' });
});
app.listen(8000);
Phase 6F Checklist
UBA-029: Validation CLI
validatecommandtestcommandinitcommandtypescommand- npm package published
- README documentation
UBA-030: Schema Gallery (Stretch)
- Gallery UI (MVP)
- Hardcoded backend list
- Add from gallery
- Submit link
- Future design documented
Community Resources
- Example templates created
- Contributing guide
- Issue templates
- Discord/forum setup
Success Criteria
- CLI tool usable by community
- At least 3 example backends
- Documentation enables self-service
- Gallery concept validated (if implemented)
Future Roadmap (Post Phase 6)
Phase 7: Docker Integration
Connect UBA with Docker for one-click backend deployment:
User selects backend in gallery
↓
Nodegx pulls Docker image
↓
Spins up containers
↓
Backend auto-configured
↓
User starts building
Phase 8: Backend Marketplace
- Verified publisher program
- Paid backends
- Usage analytics
- Revenue sharing
Phase 9: UBA SDK
- Official SDKs for Python, Node.js, Go
- Middleware for debug instrumentation
- Testing utilities
Notes
- CLI tool is highest priority in this phase
- Gallery can start as simple static list
- Community building is ongoing effort
- Success depends on documentation quality