From c0c6d5ac29ee7c9d7eb272782b9d8f1fa7c03680 Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Sat, 21 Mar 2026 19:28:56 +1100 Subject: [PATCH 1/6] feat(catalog): add @lucid-agents/catalog package for YAML/CSV route generation New extension package that generates entrypoint routes from YAML or CSV product catalog files, eliminating the need to manually define hundreds of routes for large product catalogs. Supports x402 and MPP pricing, custom handler factories, key prefixes, network overrides, and metadata. 39 tests passing across schema validation, YAML/CSV parsing, entrypoint generation, and extension integration. --- bun.lock | 20 + packages/catalog/package.json | 45 ++ .../catalog/src/__tests__/catalog.test.ts | 468 ++++++++++++++++++ packages/catalog/src/entrypoints.ts | 48 ++ packages/catalog/src/extension.ts | 63 +++ packages/catalog/src/index.ts | 9 + packages/catalog/src/parser.ts | 85 ++++ packages/catalog/src/types.ts | 34 ++ packages/catalog/tsconfig.build.json | 5 + packages/catalog/tsconfig.json | 8 + packages/catalog/tsup.config.ts | 13 + 11 files changed, 798 insertions(+) create mode 100644 packages/catalog/package.json create mode 100644 packages/catalog/src/__tests__/catalog.test.ts create mode 100644 packages/catalog/src/entrypoints.ts create mode 100644 packages/catalog/src/extension.ts create mode 100644 packages/catalog/src/index.ts create mode 100644 packages/catalog/src/parser.ts create mode 100644 packages/catalog/src/types.ts create mode 100644 packages/catalog/tsconfig.build.json create mode 100644 packages/catalog/tsconfig.json create mode 100644 packages/catalog/tsup.config.ts diff --git a/bun.lock b/bun.lock index 2c8ebd05..9462a99d 100644 --- a/bun.lock +++ b/bun.lock @@ -117,6 +117,22 @@ "@tanstack/react-query", ], }, + "packages/catalog": { + "name": "@lucid-agents/catalog", + "version": "0.1.0", + "dependencies": { + "@lucid-agents/types": "workspace:*", + "csv-parse": "^5.6.0", + "yaml": "^2.7.1", + "zod": "catalog:", + }, + "devDependencies": { + "@lucid-agents/core": "workspace:*", + "@lucid-agents/http": "workspace:*", + "tsup": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@lucid-agents/cli", "version": "2.5.0", @@ -702,6 +718,8 @@ "@lucid-agents/api-sdk": ["@lucid-agents/api-sdk@workspace:packages/api-sdk"], + "@lucid-agents/catalog": ["@lucid-agents/catalog@workspace:packages/catalog"], + "@lucid-agents/cli": ["@lucid-agents/cli@workspace:packages/cli"], "@lucid-agents/core": ["@lucid-agents/core@workspace:packages/core"], @@ -1616,6 +1634,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "csv-parse": ["csv-parse@5.6.0", "", {}, "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], diff --git a/packages/catalog/package.json b/packages/catalog/package.json new file mode 100644 index 00000000..405edfaa --- /dev/null +++ b/packages/catalog/package.json @@ -0,0 +1,45 @@ +{ + "name": "@lucid-agents/catalog", + "version": "0.1.0", + "description": "YAML/CSV catalog-driven route generation for Lucid Agents", + "repository": { + "type": "git", + "url": "https://github.com/daydreamsai/lucid-agents", + "directory": "packages/catalog" + }, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": ["dist"], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "clean": "rm -rf dist", + "type-check": "tsc -p tsconfig.json --noEmit", + "test": "bun test" + }, + "dependencies": { + "@lucid-agents/types": "workspace:*", + "yaml": "^2.7.1", + "csv-parse": "^5.6.0", + "zod": "catalog:" + }, + "devDependencies": { + "tsup": "catalog:", + "typescript": "catalog:", + "@lucid-agents/core": "workspace:*", + "@lucid-agents/http": "workspace:*" + } +} diff --git a/packages/catalog/src/__tests__/catalog.test.ts b/packages/catalog/src/__tests__/catalog.test.ts new file mode 100644 index 00000000..7cf4d138 --- /dev/null +++ b/packages/catalog/src/__tests__/catalog.test.ts @@ -0,0 +1,468 @@ +import { describe, expect, it, beforeEach, afterEach } from 'bun:test'; +import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { z } from 'zod'; + +// Types we'll implement +import { + parseCatalogYaml, + parseCatalogCsv, + type CatalogItem, + type CatalogConfig, + CatalogItemSchema, + generateEntrypoints, + catalog, +} from '../index'; + +describe('CatalogItemSchema', () => { + it('validates a complete catalog item', () => { + const item = { + key: 'product-1', + name: 'Premium API Call', + description: 'Get premium data', + price: '0.50', + }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(true); + }); + + it('validates item with invoke/stream pricing', () => { + const item = { + key: 'product-2', + name: 'Streaming API', + description: 'Stream data', + price: { invoke: '1.00', stream: '0.10' }, + }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(true); + }); + + it('validates item with metadata', () => { + const item = { + key: 'product-3', + name: 'Custom Product', + description: 'A product', + price: '2.00', + metadata: { category: 'ai', tier: 'premium' }, + }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(true); + }); + + it('rejects item without key', () => { + const item = { name: 'No Key', price: '1.00' }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(false); + }); + + it('rejects item without name', () => { + const item = { key: 'no-name', price: '1.00' }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(false); + }); + + it('allows item without price (free)', () => { + const item = { key: 'free-item', name: 'Free Thing', description: 'Free' }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(true); + }); + + it('validates item with network field', () => { + const item = { + key: 'net-item', + name: 'Network Product', + price: '1.00', + network: 'base-sepolia', + }; + const result = CatalogItemSchema.safeParse(item); + expect(result.success).toBe(true); + }); +}); + +describe('parseCatalogYaml', () => { + it('parses a simple YAML catalog', () => { + const yamlContent = ` +products: + - key: widget-a + name: Widget A + description: A fine widget + price: "1.00" + - key: widget-b + name: Widget B + description: Another widget + price: "2.50" +`; + const items = parseCatalogYaml(yamlContent); + expect(items).toHaveLength(2); + expect(items[0].key).toBe('widget-a'); + expect(items[0].name).toBe('Widget A'); + expect(items[0].price).toBe('1.00'); + expect(items[1].key).toBe('widget-b'); + expect(items[1].price).toBe('2.50'); + }); + + it('parses YAML with invoke/stream pricing', () => { + const yamlContent = ` +products: + - key: streaming-api + name: Streaming API + description: Streamed data + price: + invoke: "5.00" + stream: "0.50" +`; + const items = parseCatalogYaml(yamlContent); + expect(items).toHaveLength(1); + expect(items[0].price).toEqual({ invoke: '5.00', stream: '0.50' }); + }); + + it('parses YAML with metadata', () => { + const yamlContent = ` +products: + - key: premium + name: Premium + description: Premium tier + price: "10.00" + metadata: + tier: premium + rateLimit: 1000 +`; + const items = parseCatalogYaml(yamlContent); + expect(items[0].metadata).toEqual({ tier: 'premium', rateLimit: 1000 }); + }); + + it('parses YAML with network field', () => { + const yamlContent = ` +products: + - key: solana-product + name: Solana Product + price: "1.00" + network: solana-devnet +`; + const items = parseCatalogYaml(yamlContent); + expect(items[0].network).toBe('solana-devnet'); + }); + + it('supports flat array format (no products wrapper)', () => { + const yamlContent = ` +- key: item-1 + name: Item 1 + price: "1.00" +- key: item-2 + name: Item 2 + price: "2.00" +`; + const items = parseCatalogYaml(yamlContent); + expect(items).toHaveLength(2); + }); + + it('throws on invalid YAML structure', () => { + expect(() => parseCatalogYaml('not: valid: yaml: [')).toThrow(); + }); + + it('throws on items missing required fields', () => { + const yamlContent = ` +products: + - description: no key or name + price: "1.00" +`; + expect(() => parseCatalogYaml(yamlContent)).toThrow(); + }); +}); + +describe('parseCatalogCsv', () => { + it('parses a simple CSV catalog', async () => { + const csvContent = `key,name,description,price +widget-a,Widget A,A fine widget,1.00 +widget-b,Widget B,Another widget,2.50`; + const items = await parseCatalogCsv(csvContent); + expect(items).toHaveLength(2); + expect(items[0].key).toBe('widget-a'); + expect(items[0].name).toBe('Widget A'); + expect(items[0].price).toBe('1.00'); + }); + + it('handles quoted fields with commas', async () => { + const csvContent = `key,name,description,price +api-1,"Advanced API","AI-powered, real-time data",3.00`; + const items = await parseCatalogCsv(csvContent); + expect(items[0].description).toBe('AI-powered, real-time data'); + }); + + it('handles empty price as free', async () => { + const csvContent = `key,name,description,price +free-item,Free Item,No cost,`; + const items = await parseCatalogCsv(csvContent); + expect(items[0].price).toBeUndefined(); + }); + + it('parses CSV with network column', async () => { + const csvContent = `key,name,description,price,network +sol-item,Sol Item,On Solana,1.00,solana-devnet`; + const items = await parseCatalogCsv(csvContent); + expect(items[0].network).toBe('solana-devnet'); + }); + + it('parses CSV with metadata columns (prefixed with meta_)', async () => { + const csvContent = `key,name,description,price,meta_category,meta_tier +premium,Premium,Top tier,10.00,ai,premium`; + const items = await parseCatalogCsv(csvContent); + expect(items[0].metadata).toEqual({ category: 'ai', tier: 'premium' }); + }); + + it('throws on CSV missing key column', async () => { + const csvContent = `name,description,price +Widget,A widget,1.00`; + await expect(parseCatalogCsv(csvContent)).rejects.toThrow(); + }); +}); + +describe('generateEntrypoints', () => { + const sampleItems: CatalogItem[] = [ + { key: 'widget-a', name: 'Widget A', description: 'A widget', price: '1.00' }, + { key: 'widget-b', name: 'Widget B', description: 'B widget', price: '2.50' }, + ]; + + it('generates one entrypoint per catalog item', () => { + const entrypoints = generateEntrypoints(sampleItems); + expect(entrypoints).toHaveLength(2); + expect(entrypoints[0].key).toBe('widget-a'); + expect(entrypoints[1].key).toBe('widget-b'); + }); + + it('sets price on entrypoints', () => { + const entrypoints = generateEntrypoints(sampleItems); + expect(entrypoints[0].price).toBe('1.00'); + expect(entrypoints[1].price).toBe('2.50'); + }); + + it('sets description from catalog item', () => { + const entrypoints = generateEntrypoints(sampleItems); + expect(entrypoints[0].description).toBe('A widget'); + }); + + it('generates entrypoints with invoke/stream pricing', () => { + const items: CatalogItem[] = [ + { key: 'stream-api', name: 'Stream', price: { invoke: '5.00', stream: '0.50' } }, + ]; + const entrypoints = generateEntrypoints(items); + expect(entrypoints[0].price).toEqual({ invoke: '5.00', stream: '0.50' }); + }); + + it('generates entrypoints without price for free items', () => { + const items: CatalogItem[] = [ + { key: 'free-item', name: 'Free' }, + ]; + const entrypoints = generateEntrypoints(items); + expect(entrypoints[0].price).toBeUndefined(); + }); + + it('passes metadata through to entrypoint', () => { + const items: CatalogItem[] = [ + { key: 'meta-item', name: 'Meta', price: '1.00', metadata: { tier: 'gold' } }, + ]; + const entrypoints = generateEntrypoints(items); + expect(entrypoints[0].metadata).toEqual({ tier: 'gold', catalogItem: items[0] }); + }); + + it('applies custom handler factory when provided', () => { + const handlerFactory = (item: CatalogItem) => { + return async (ctx: any) => ({ output: { product: item.key } }); + }; + const entrypoints = generateEntrypoints(sampleItems, { handlerFactory }); + expect(entrypoints[0].handler).toBeDefined(); + }); + + it('applies key prefix when provided', () => { + const entrypoints = generateEntrypoints(sampleItems, { keyPrefix: 'shop/' }); + expect(entrypoints[0].key).toBe('shop/widget-a'); + expect(entrypoints[1].key).toBe('shop/widget-b'); + }); + + it('applies network override to all entrypoints', () => { + const entrypoints = generateEntrypoints(sampleItems, { network: 'base-sepolia' }); + expect(entrypoints[0].network).toBe('base-sepolia'); + expect(entrypoints[1].network).toBe('base-sepolia'); + }); + + it('item-level network overrides global network', () => { + const items: CatalogItem[] = [ + { key: 'item', name: 'Item', price: '1.00', network: 'solana-devnet' }, + ]; + const entrypoints = generateEntrypoints(items, { network: 'base-sepolia' }); + expect(entrypoints[0].network).toBe('solana-devnet'); + }); + + it('sets default input schema with product key', () => { + const entrypoints = generateEntrypoints(sampleItems); + // Should have a basic input schema + expect(entrypoints[0].input).toBeDefined(); + }); + + it('applies custom input schema when provided', () => { + const customInput = z.object({ quantity: z.number() }); + const entrypoints = generateEntrypoints(sampleItems, { inputSchema: customInput }); + expect(entrypoints[0].input).toBe(customInput); + }); +}); + +describe('catalog extension', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `catalog-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + if (existsSync(tmpDir)) { + rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('creates a valid extension object', () => { + const yamlPath = join(tmpDir, 'products.yaml'); + writeFileSync(yamlPath, ` +products: + - key: test + name: Test + price: "1.00" +`); + const ext = catalog({ file: yamlPath }); + expect(ext.name).toBe('catalog'); + expect(ext.build).toBeDefined(); + }); + + it('build returns catalog runtime with items', () => { + const yamlPath = join(tmpDir, 'products.yaml'); + writeFileSync(yamlPath, ` +products: + - key: test-product + name: Test Product + price: "1.00" + - key: another + name: Another + price: "2.00" +`); + const ext = catalog({ file: yamlPath }); + const result = ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + expect(result.catalog).toBeDefined(); + expect(result.catalog!.items).toHaveLength(2); + expect(result.catalog!.items[0].key).toBe('test-product'); + }); + + it('onBuild registers entrypoints from catalog', async () => { + const yamlPath = join(tmpDir, 'products.yaml'); + writeFileSync(yamlPath, ` +products: + - key: alpha + name: Alpha + description: Alpha product + price: "1.00" + - key: beta + name: Beta + description: Beta product + price: "2.00" +`); + const ext = catalog({ file: yamlPath }); + ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + + // Simulate runtime with entrypoints.add + const added: any[] = []; + const mockRuntime = { + entrypoints: { + add: (def: any) => added.push(def), + list: () => [], + snapshot: () => [], + }, + } as any; + + await ext.onBuild!(mockRuntime); + expect(added).toHaveLength(2); + expect(added[0].key).toBe('alpha'); + expect(added[0].price).toBe('1.00'); + expect(added[1].key).toBe('beta'); + }); + + it('loads CSV files', async () => { + const csvPath = join(tmpDir, 'products.csv'); + writeFileSync(csvPath, `key,name,description,price +item-1,Item 1,First item,1.00 +item-2,Item 2,Second item,2.00`); + const ext = catalog({ file: csvPath }); + // CSV loading is async, happens in onBuild + ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + + const added: any[] = []; + const mockRuntime = { + entrypoints: { + add: (def: any) => added.push(def), + list: () => [], + snapshot: () => [], + }, + } as any; + + await ext.onBuild!(mockRuntime); + expect(added).toHaveLength(2); + expect(added[0].key).toBe('item-1'); + }); + + it('applies handlerFactory from options', async () => { + const yamlPath = join(tmpDir, 'products.yaml'); + writeFileSync(yamlPath, ` +products: + - key: test + name: Test + price: "1.00" +`); + const handlerFactory = (item: CatalogItem) => { + return async () => ({ output: { product: item.key, price: item.price } }); + }; + const ext = catalog({ file: yamlPath, handlerFactory }); + ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + + const added: any[] = []; + const mockRuntime = { + entrypoints: { + add: (def: any) => added.push(def), + list: () => [], + snapshot: () => [], + }, + } as any; + + await ext.onBuild!(mockRuntime); + expect(added[0].handler).toBeDefined(); + const result = await added[0].handler({}); + expect(result.output.product).toBe('test'); + }); + + it('applies keyPrefix from options', async () => { + const yamlPath = join(tmpDir, 'products.yaml'); + writeFileSync(yamlPath, ` +products: + - key: test + name: Test + price: "1.00" +`); + const ext = catalog({ file: yamlPath, keyPrefix: 'store/' }); + ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + + const added: any[] = []; + const mockRuntime = { + entrypoints: { + add: (def: any) => added.push(def), + list: () => [], + snapshot: () => [], + }, + } as any; + + await ext.onBuild!(mockRuntime); + expect(added[0].key).toBe('store/test'); + }); + + it('throws if file does not exist', () => { + const ext = catalog({ file: '/nonexistent/path.yaml' }); + expect(() => ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} })).toThrow(); + }); +}); diff --git a/packages/catalog/src/entrypoints.ts b/packages/catalog/src/entrypoints.ts new file mode 100644 index 00000000..051cdbbf --- /dev/null +++ b/packages/catalog/src/entrypoints.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; +import type { CatalogItem, HandlerFactory } from './types'; + +export type GenerateOptions = { + keyPrefix?: string; + network?: string; + handlerFactory?: HandlerFactory; + inputSchema?: z.ZodTypeAny; +}; + +const defaultInputSchema = z.object({ + params: z.record(z.string(), z.unknown()).optional(), +}); + +export function generateEntrypoints( + items: CatalogItem[], + options?: GenerateOptions, +): any[] { + const { keyPrefix, network, handlerFactory, inputSchema } = options ?? {}; + + return items.map((item) => { + const key = keyPrefix ? `${keyPrefix}${item.key}` : item.key; + const entrypointNetwork = item.network ?? network; + + const metadata = { + ...item.metadata, + catalogItem: item, + }; + + const entrypoint: Record = { + key, + description: item.description, + price: item.price, + input: inputSchema ?? defaultInputSchema, + metadata, + }; + + if (entrypointNetwork) { + entrypoint.network = entrypointNetwork; + } + + if (handlerFactory) { + entrypoint.handler = handlerFactory(item); + } + + return entrypoint; + }); +} diff --git a/packages/catalog/src/extension.ts b/packages/catalog/src/extension.ts new file mode 100644 index 00000000..ca5d9fbf --- /dev/null +++ b/packages/catalog/src/extension.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'fs'; +import { extname } from 'path'; +import type { CatalogItem, CatalogExtensionOptions } from './types'; +import { parseCatalogYaml, parseCatalogCsv } from './parser'; +import { generateEntrypoints } from './entrypoints'; + +export type CatalogRuntime = { + items: CatalogItem[]; +}; + +export function catalog(options: CatalogExtensionOptions): { + name: string; + build: (ctx: any) => { catalog?: CatalogRuntime }; + onBuild?: (runtime: any) => Promise; +} { + let catalogItems: CatalogItem[] = []; + let pendingCsvParse: Promise | null = null; + + return { + name: 'catalog', + build(ctx: any): { catalog?: CatalogRuntime } { + const ext = extname(options.file).toLowerCase(); + + if (ext === '.yaml' || ext === '.yml') { + const content = readFileSync(options.file, 'utf-8'); + catalogItems = parseCatalogYaml(content); + } else if (ext === '.csv') { + // CSV parsing is async, defer to onBuild + const content = readFileSync(options.file, 'utf-8'); + pendingCsvParse = parseCatalogCsv(content); + } else { + throw new Error( + `Unsupported catalog file format: ${ext}. Use .yaml, .yml, or .csv`, + ); + } + + return { + catalog: { + items: catalogItems, + }, + }; + }, + async onBuild(runtime: any): Promise { + // Resolve CSV if needed + if (pendingCsvParse) { + catalogItems = await pendingCsvParse; + pendingCsvParse = null; + } + + // Generate and register entrypoints + const entrypoints = generateEntrypoints(catalogItems, { + keyPrefix: options.keyPrefix, + network: options.network, + handlerFactory: options.handlerFactory, + inputSchema: options.inputSchema, + }); + + for (const ep of entrypoints) { + runtime.entrypoints.add(ep); + } + }, + }; +} diff --git a/packages/catalog/src/index.ts b/packages/catalog/src/index.ts new file mode 100644 index 00000000..26da9778 --- /dev/null +++ b/packages/catalog/src/index.ts @@ -0,0 +1,9 @@ +export { parseCatalogYaml, parseCatalogCsv } from './parser'; +export { generateEntrypoints } from './entrypoints'; +export { catalog } from './extension'; +export { + CatalogItemSchema, + type CatalogItem, + type CatalogConfig, + type CatalogExtensionOptions, +} from './types'; diff --git a/packages/catalog/src/parser.ts b/packages/catalog/src/parser.ts new file mode 100644 index 00000000..8169ccca --- /dev/null +++ b/packages/catalog/src/parser.ts @@ -0,0 +1,85 @@ +import YAML from 'yaml'; +import { parse as csvParse } from 'csv-parse/sync'; +import { CatalogItemSchema, type CatalogItem } from './types'; + +export function parseCatalogYaml(content: string): CatalogItem[] { + const parsed = YAML.parse(content); + + let rawItems: unknown[]; + + if (Array.isArray(parsed)) { + rawItems = parsed; + } else if ( + parsed && + typeof parsed === 'object' && + 'products' in parsed && + Array.isArray(parsed.products) + ) { + rawItems = parsed.products; + } else { + throw new Error( + 'YAML must contain a "products" array or be a top-level array', + ); + } + + const items: CatalogItem[] = []; + for (const raw of rawItems) { + const result = CatalogItemSchema.safeParse(raw); + if (!result.success) { + throw new Error(`Invalid catalog item: ${result.error.message}`); + } + items.push(result.data); + } + + return items; +} + +export async function parseCatalogCsv( + content: string, +): Promise { + const records = csvParse(content, { + columns: true, + skip_empty_lines: true, + trim: true, + }) as Record[]; + + // Validate key column exists + if (records.length > 0 && !('key' in records[0])) { + throw new Error('CSV must have a "key" column'); + } + + const items: CatalogItem[] = []; + for (const record of records) { + const metadata: Record = {}; + let hasMetadata = false; + + for (const [col, val] of Object.entries(record)) { + if (col.startsWith('meta_')) { + metadata[col.slice(5)] = val; + hasMetadata = true; + } + } + + const item: CatalogItem = { + key: record.key, + name: record.name, + description: record.description || undefined, + price: + record.price && record.price.trim() !== '' + ? record.price + : undefined, + network: record.network || undefined, + ...(hasMetadata ? { metadata } : {}), + }; + + const result = CatalogItemSchema.safeParse(item); + if (!result.success) { + throw new Error( + `Invalid CSV row for key "${record.key}": ${result.error.message}`, + ); + } + items.push(result.data); + } + + return items; +} diff --git a/packages/catalog/src/types.ts b/packages/catalog/src/types.ts new file mode 100644 index 00000000..e8aabbee --- /dev/null +++ b/packages/catalog/src/types.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +export const CatalogItemSchema = z.object({ + key: z.string(), + name: z.string(), + description: z.string().optional(), + price: z + .union([ + z.string(), + z.object({ + invoke: z.string().optional(), + stream: z.string().optional(), + }), + ]) + .optional(), + network: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}); + +export type CatalogItem = z.infer; + +export type CatalogConfig = { + items: CatalogItem[]; +}; + +export type HandlerFactory = (item: CatalogItem) => (...args: any[]) => any; + +export type CatalogExtensionOptions = { + file: string; + keyPrefix?: string; + network?: string; + handlerFactory?: HandlerFactory; + inputSchema?: z.ZodTypeAny; +}; diff --git a/packages/catalog/tsconfig.build.json b/packages/catalog/tsconfig.build.json new file mode 100644 index 00000000..39b29485 --- /dev/null +++ b/packages/catalog/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.build.base.json", + "include": ["src/**/*"], + "exclude": ["dist", "node_modules", "**/*.test.ts", "**/__tests__/**"] +} diff --git a/packages/catalog/tsconfig.json b/packages/catalog/tsconfig.json new file mode 100644 index 00000000..a6098995 --- /dev/null +++ b/packages/catalog/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "composite": true + }, + "include": ["src/**/*"] +} diff --git a/packages/catalog/tsup.config.ts b/packages/catalog/tsup.config.ts new file mode 100644 index 00000000..92f8226b --- /dev/null +++ b/packages/catalog/tsup.config.ts @@ -0,0 +1,13 @@ +import { definePackageConfig } from '../tsup.config.base'; + +export default definePackageConfig({ + entry: ['src/index.ts'], + dts: true, + external: [ + '@lucid-agents/core', + '@lucid-agents/types', + 'yaml', + 'csv-parse', + 'zod', + ], +}); From 99c2e0bb4541e28be3740ad70789ff41d03632aa Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Sat, 21 Mar 2026 22:53:52 +1100 Subject: [PATCH 2/6] fix(catalog): address code review feedback - Make parseCatalogCsv synchronous (csv-parse/sync is already sync) - Fix CSV catalog.items being empty in runtime slice (no longer deferred) - Type extension with Extension interface, BuildContext, AgentRuntime - Type generateEntrypoints return as EntrypointDef[] instead of any[] - Use CAIP-2 network format in tests (solana:devnet, eip155:84532) --- .../catalog/src/__tests__/catalog.test.ts | 55 ++++++++++--------- packages/catalog/src/entrypoints.ts | 16 +++--- packages/catalog/src/extension.ts | 31 ++++------- packages/catalog/src/parser.ts | 4 +- 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/packages/catalog/src/__tests__/catalog.test.ts b/packages/catalog/src/__tests__/catalog.test.ts index 7cf4d138..6721c976 100644 --- a/packages/catalog/src/__tests__/catalog.test.ts +++ b/packages/catalog/src/__tests__/catalog.test.ts @@ -73,7 +73,7 @@ describe('CatalogItemSchema', () => { key: 'net-item', name: 'Network Product', price: '1.00', - network: 'base-sepolia', + network: 'eip155:84532', }; const result = CatalogItemSchema.safeParse(item); expect(result.success).toBe(true); @@ -138,10 +138,10 @@ products: - key: solana-product name: Solana Product price: "1.00" - network: solana-devnet + network: solana:devnet `; const items = parseCatalogYaml(yamlContent); - expect(items[0].network).toBe('solana-devnet'); + expect(items[0].network).toBe('solana:devnet'); }); it('supports flat array format (no products wrapper)', () => { @@ -172,49 +172,49 @@ products: }); describe('parseCatalogCsv', () => { - it('parses a simple CSV catalog', async () => { + it('parses a simple CSV catalog', () => { const csvContent = `key,name,description,price widget-a,Widget A,A fine widget,1.00 widget-b,Widget B,Another widget,2.50`; - const items = await parseCatalogCsv(csvContent); + const items = parseCatalogCsv(csvContent); expect(items).toHaveLength(2); expect(items[0].key).toBe('widget-a'); expect(items[0].name).toBe('Widget A'); expect(items[0].price).toBe('1.00'); }); - it('handles quoted fields with commas', async () => { + it('handles quoted fields with commas', () => { const csvContent = `key,name,description,price api-1,"Advanced API","AI-powered, real-time data",3.00`; - const items = await parseCatalogCsv(csvContent); + const items = parseCatalogCsv(csvContent); expect(items[0].description).toBe('AI-powered, real-time data'); }); - it('handles empty price as free', async () => { + it('handles empty price as free', () => { const csvContent = `key,name,description,price free-item,Free Item,No cost,`; - const items = await parseCatalogCsv(csvContent); + const items = parseCatalogCsv(csvContent); expect(items[0].price).toBeUndefined(); }); - it('parses CSV with network column', async () => { + it('parses CSV with network column', () => { const csvContent = `key,name,description,price,network -sol-item,Sol Item,On Solana,1.00,solana-devnet`; - const items = await parseCatalogCsv(csvContent); - expect(items[0].network).toBe('solana-devnet'); +sol-item,Sol Item,On Solana,1.00,solana:devnet`; + const items = parseCatalogCsv(csvContent); + expect(items[0].network).toBe('solana:devnet'); }); - it('parses CSV with metadata columns (prefixed with meta_)', async () => { + it('parses CSV with metadata columns (prefixed with meta_)', () => { const csvContent = `key,name,description,price,meta_category,meta_tier premium,Premium,Top tier,10.00,ai,premium`; - const items = await parseCatalogCsv(csvContent); + const items = parseCatalogCsv(csvContent); expect(items[0].metadata).toEqual({ category: 'ai', tier: 'premium' }); }); - it('throws on CSV missing key column', async () => { + it('throws on CSV missing key column', () => { const csvContent = `name,description,price Widget,A widget,1.00`; - await expect(parseCatalogCsv(csvContent)).rejects.toThrow(); + expect(() => parseCatalogCsv(csvContent)).toThrow(); }); }); @@ -281,17 +281,17 @@ describe('generateEntrypoints', () => { }); it('applies network override to all entrypoints', () => { - const entrypoints = generateEntrypoints(sampleItems, { network: 'base-sepolia' }); - expect(entrypoints[0].network).toBe('base-sepolia'); - expect(entrypoints[1].network).toBe('base-sepolia'); + const entrypoints = generateEntrypoints(sampleItems, { network: 'eip155:84532' }); + expect(entrypoints[0].network).toBe('eip155:84532'); + expect(entrypoints[1].network).toBe('eip155:84532'); }); it('item-level network overrides global network', () => { const items: CatalogItem[] = [ - { key: 'item', name: 'Item', price: '1.00', network: 'solana-devnet' }, + { key: 'item', name: 'Item', price: '1.00', network: 'solana:devnet' }, ]; - const entrypoints = generateEntrypoints(items, { network: 'base-sepolia' }); - expect(entrypoints[0].network).toBe('solana-devnet'); + const entrypoints = generateEntrypoints(items, { network: 'eip155:84532' }); + expect(entrypoints[0].network).toBe('solana:devnet'); }); it('sets default input schema with product key', () => { @@ -385,14 +385,17 @@ products: expect(added[1].key).toBe('beta'); }); - it('loads CSV files', async () => { + it('loads CSV files and populates catalog.items', async () => { const csvPath = join(tmpDir, 'products.csv'); writeFileSync(csvPath, `key,name,description,price item-1,Item 1,First item,1.00 item-2,Item 2,Second item,2.00`); const ext = catalog({ file: csvPath }); - // CSV loading is async, happens in onBuild - ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + const result = ext.build({ meta: { name: 'test', version: '1.0.0' }, runtime: {} }); + + // CSV is now parsed synchronously — catalog.items should be populated immediately + expect(result.catalog!.items).toHaveLength(2); + expect(result.catalog!.items[0].key).toBe('item-1'); const added: any[] = []; const mockRuntime = { diff --git a/packages/catalog/src/entrypoints.ts b/packages/catalog/src/entrypoints.ts index 051cdbbf..a5666af4 100644 --- a/packages/catalog/src/entrypoints.ts +++ b/packages/catalog/src/entrypoints.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { EntrypointDef } from '@lucid-agents/types/core'; import type { CatalogItem, HandlerFactory } from './types'; export type GenerateOptions = { @@ -15,10 +16,10 @@ const defaultInputSchema = z.object({ export function generateEntrypoints( items: CatalogItem[], options?: GenerateOptions, -): any[] { +): EntrypointDef[] { const { keyPrefix, network, handlerFactory, inputSchema } = options ?? {}; - return items.map((item) => { + return items.map((item): EntrypointDef => { const key = keyPrefix ? `${keyPrefix}${item.key}` : item.key; const entrypointNetwork = item.network ?? network; @@ -27,20 +28,17 @@ export function generateEntrypoints( catalogItem: item, }; - const entrypoint: Record = { + const entrypoint: EntrypointDef = { key, description: item.description, - price: item.price, + price: item.price as EntrypointDef['price'], input: inputSchema ?? defaultInputSchema, metadata, + ...(entrypointNetwork ? { network: entrypointNetwork as EntrypointDef['network'] } : {}), }; - if (entrypointNetwork) { - entrypoint.network = entrypointNetwork; - } - if (handlerFactory) { - entrypoint.handler = handlerFactory(item); + entrypoint.handler = handlerFactory(item) as EntrypointDef['handler']; } return entrypoint; diff --git a/packages/catalog/src/extension.ts b/packages/catalog/src/extension.ts index ca5d9fbf..fd8b011d 100644 --- a/packages/catalog/src/extension.ts +++ b/packages/catalog/src/extension.ts @@ -1,5 +1,10 @@ import { readFileSync } from 'fs'; import { extname } from 'path'; +import type { + Extension, + BuildContext, + AgentRuntime, +} from '@lucid-agents/types/core'; import type { CatalogItem, CatalogExtensionOptions } from './types'; import { parseCatalogYaml, parseCatalogCsv } from './parser'; import { generateEntrypoints } from './entrypoints'; @@ -8,26 +13,21 @@ export type CatalogRuntime = { items: CatalogItem[]; }; -export function catalog(options: CatalogExtensionOptions): { - name: string; - build: (ctx: any) => { catalog?: CatalogRuntime }; - onBuild?: (runtime: any) => Promise; -} { +export function catalog( + options: CatalogExtensionOptions, +): Extension<{ catalog?: CatalogRuntime }> { let catalogItems: CatalogItem[] = []; - let pendingCsvParse: Promise | null = null; return { name: 'catalog', - build(ctx: any): { catalog?: CatalogRuntime } { + build(ctx: BuildContext): { catalog?: CatalogRuntime } { const ext = extname(options.file).toLowerCase(); + const content = readFileSync(options.file, 'utf-8'); if (ext === '.yaml' || ext === '.yml') { - const content = readFileSync(options.file, 'utf-8'); catalogItems = parseCatalogYaml(content); } else if (ext === '.csv') { - // CSV parsing is async, defer to onBuild - const content = readFileSync(options.file, 'utf-8'); - pendingCsvParse = parseCatalogCsv(content); + catalogItems = parseCatalogCsv(content); } else { throw new Error( `Unsupported catalog file format: ${ext}. Use .yaml, .yml, or .csv`, @@ -40,14 +40,7 @@ export function catalog(options: CatalogExtensionOptions): { }, }; }, - async onBuild(runtime: any): Promise { - // Resolve CSV if needed - if (pendingCsvParse) { - catalogItems = await pendingCsvParse; - pendingCsvParse = null; - } - - // Generate and register entrypoints + async onBuild(runtime: AgentRuntime): Promise { const entrypoints = generateEntrypoints(catalogItems, { keyPrefix: options.keyPrefix, network: options.network, diff --git a/packages/catalog/src/parser.ts b/packages/catalog/src/parser.ts index 8169ccca..027b81ca 100644 --- a/packages/catalog/src/parser.ts +++ b/packages/catalog/src/parser.ts @@ -34,9 +34,9 @@ export function parseCatalogYaml(content: string): CatalogItem[] { return items; } -export async function parseCatalogCsv( +export function parseCatalogCsv( content: string, -): Promise { +): CatalogItem[] { const records = csvParse(content, { columns: true, skip_empty_lines: true, From 9afbd3fe04438b56667f144065817dc67048a28d Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Sat, 21 Mar 2026 22:59:25 +1100 Subject: [PATCH 3/6] docs(catalog): add README and catalog+MPP example - Comprehensive README with API reference, YAML/CSV format docs, integration examples (x402, MPP, handler factories, key prefixes) - Example: catalog-mpp-store.ts showing 10-product YAML catalog with MPP tempo payments and handler factory - Example: products.csv showing CSV format with meta_ columns - Add @lucid-agents/catalog to examples dependencies --- bun.lock | 1 + packages/catalog/README.md | 391 ++++++++++++++++++ packages/examples/package.json | 1 + .../examples/src/catalog/catalog-mpp-store.ts | 127 ++++++ packages/examples/src/catalog/products.csv | 6 + packages/examples/src/catalog/products.yaml | 106 +++++ 6 files changed, 632 insertions(+) create mode 100644 packages/catalog/README.md create mode 100644 packages/examples/src/catalog/catalog-mpp-store.ts create mode 100644 packages/examples/src/catalog/products.csv create mode 100644 packages/examples/src/catalog/products.yaml diff --git a/bun.lock b/bun.lock index 9462a99d..b2d17e79 100644 --- a/bun.lock +++ b/bun.lock @@ -189,6 +189,7 @@ "@lucid-agents/a2a": "workspace:*", "@lucid-agents/analytics": "workspace:*", "@lucid-agents/ap2": "workspace:*", + "@lucid-agents/catalog": "workspace:*", "@lucid-agents/core": "workspace:*", "@lucid-agents/hono": "workspace:*", "@lucid-agents/http": "workspace:*", diff --git a/packages/catalog/README.md b/packages/catalog/README.md new file mode 100644 index 00000000..7c515c03 --- /dev/null +++ b/packages/catalog/README.md @@ -0,0 +1,391 @@ +# @lucid-agents/catalog + +YAML/CSV catalog-driven route generation for Lucid Agents. + +## Overview + +When your agent exposes a large product catalog -- hundreds or thousands of items -- manually calling `addEntrypoint()` for each one becomes unmanageable. Writing 400 entrypoint definitions by hand is tedious, error-prone, and hard to keep in sync with your actual inventory. + +`@lucid-agents/catalog` solves this by letting you define your catalog in a YAML or CSV file and automatically generating typed entrypoints at build time. A single `.use(catalog(...))` call replaces hundreds of manual `addEntrypoint()` calls. + +Key features: + +- **YAML and CSV parsing** -- Define products in the format that fits your workflow +- **Automatic entrypoint generation** -- Each catalog item becomes a discoverable, invocable route +- **Price support** -- Flat prices or separate invoke/stream prices per item +- **Network tagging** -- Per-item or global network assignment in CAIP-2 format +- **Custom metadata** -- Attach arbitrary key-value pairs to catalog items +- **Handler factories** -- Generate route handlers dynamically from catalog data +- **Key prefixing** -- Namespace catalog routes to avoid collisions + +## Installation + +```bash +bun add @lucid-agents/catalog +``` + +## Quick Start + +Define a catalog file (`products.yaml`): + +```yaml +products: + - key: weather + name: Weather Lookup + description: Get current weather for a city + price: "0.001" + - key: translate + name: Translation + description: Translate text between languages + price: "0.002" +``` + +Wire it into your agent: + +```typescript +import { createAgent } from '@lucid-agents/core'; +import { http } from '@lucid-agents/http'; +import { catalog } from '@lucid-agents/catalog'; +import { createAgentApp } from '@lucid-agents/hono'; + +const agent = await createAgent({ + name: 'catalog-agent', + version: '1.0.0', + description: 'Agent with catalog-driven routes', +}) + .use(http()) + .use(catalog({ file: './products.yaml' })) + .build(); + +const { app } = await createAgentApp(agent); +export default app; +``` + +Each item in `products.yaml` is now a discoverable entrypoint on your agent. + +## YAML Format + +YAML catalogs support either a top-level array or an object with a `products` key: + +```yaml +products: + - key: sentiment + name: Sentiment Analysis + description: Analyze text sentiment + price: "0.001" + network: "eip155:8453" + metadata: + category: nlp + model: gpt-4o-mini +``` + +### Supported Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `key` | `string` | Yes | Unique route identifier. Becomes the entrypoint key. | +| `name` | `string` | Yes | Human-readable display name. | +| `description` | `string` | No | Description shown in the agent manifest. | +| `price` | `string` or `object` | No | Flat price as a string, or an object with `invoke` and/or `stream` prices. | +| `network` | `string` | No | CAIP-2 network identifier (e.g., `eip155:8453`). | +| `metadata` | `object` | No | Arbitrary key-value pairs attached to the entrypoint. | + +### Price Variants + +Flat price: + +```yaml +- key: summarize + name: Summarize + price: "0.005" +``` + +Separate invoke and stream prices: + +```yaml +- key: generate + name: Generate Text + price: + invoke: "0.01" + stream: "0.002" +``` + +## CSV Format + +CSV catalogs use a header row with the standard fields. Metadata is supported via the `meta_` column prefix convention. + +```csv +key,name,description,price,network,meta_category,meta_model +weather,Weather Lookup,Get current weather,0.001,eip155:8453,utility,gpt-4o-mini +translate,Translation,Translate text,0.002,,nlp,gpt-4o +sentiment,Sentiment Analysis,Analyze sentiment,0.001,,nlp,gpt-4o-mini +``` + +### Column Reference + +| Column | Required | Description | +|--------|----------|-------------| +| `key` | Yes | Unique route identifier. | +| `name` | Yes | Display name. | +| `description` | No | Route description. | +| `price` | No | Price as a string value. | +| `network` | No | CAIP-2 network identifier. | +| `meta_*` | No | Any column prefixed with `meta_` is collected into the `metadata` object with the prefix stripped. | + +For example, a column named `meta_category` with value `nlp` results in `metadata: { category: "nlp" }`. + +Note: CSV does not support the invoke/stream price object syntax. Use YAML if you need separate prices per method. + +## API Reference + +### `catalog(options)` + +Extension factory that reads a catalog file and registers entrypoints at build time. + +```typescript +import { catalog } from '@lucid-agents/catalog'; + +agent.use(catalog({ + file: './products.yaml', + keyPrefix: 'store/', + network: 'eip155:8453', + handlerFactory: (item) => async ({ input }) => { + return { output: { product: item.key } }; + }, +})); +``` + +**`CatalogExtensionOptions`** + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `file` | `string` | Yes | Path to the `.yaml`, `.yml`, or `.csv` catalog file. | +| `keyPrefix` | `string` | No | Prefix prepended to every item key (e.g., `"store/"` turns `"widget"` into `"store/widget"`). | +| `network` | `string` | No | Default CAIP-2 network for items that do not specify their own. | +| `handlerFactory` | `HandlerFactory` | No | Function that receives a `CatalogItem` and returns a handler function. | +| `inputSchema` | `z.ZodTypeAny` | No | Zod schema applied to all generated entrypoints. Defaults to `z.object({ params: z.record(z.string(), z.unknown()).optional() })`. | + +### `parseCatalogYaml(content)` + +Parses a YAML string into an array of validated `CatalogItem` objects. + +```typescript +import { parseCatalogYaml } from '@lucid-agents/catalog'; + +const items = parseCatalogYaml(` +products: + - key: weather + name: Weather Lookup + price: "0.001" +`); +// items: CatalogItem[] +``` + +Accepts either a top-level array or an object with a `products` key. Throws if any item fails validation. + +### `parseCatalogCsv(content)` + +Parses a CSV string into an array of validated `CatalogItem` objects. + +```typescript +import { parseCatalogCsv } from '@lucid-agents/catalog'; + +const items = parseCatalogCsv(`key,name,price +weather,Weather Lookup,0.001 +translate,Translation,0.002`); +// items: CatalogItem[] +``` + +Requires a `key` column. Columns prefixed with `meta_` are collected into the `metadata` field. Throws if any row fails validation. + +### `generateEntrypoints(items, options?)` + +Converts an array of `CatalogItem` objects into `EntrypointDef` objects ready for registration. + +```typescript +import { generateEntrypoints } from '@lucid-agents/catalog'; + +const entrypoints = generateEntrypoints(items, { + keyPrefix: 'api/', + network: 'eip155:8453', + handlerFactory: (item) => async ({ input }) => { + return { output: { result: item.name } }; + }, +}); + +for (const ep of entrypoints) { + runtime.entrypoints.add(ep); +} +``` + +**`GenerateOptions`** + +| Option | Type | Description | +|--------|------|-------------| +| `keyPrefix` | `string` | Prefix prepended to each item key. | +| `network` | `string` | Default network for items without one. | +| `handlerFactory` | `HandlerFactory` | Generates a handler for each item. | +| `inputSchema` | `z.ZodTypeAny` | Input schema for all entrypoints. | + +### `CatalogItemSchema` + +Zod schema used to validate catalog items. Useful for custom parsing or validation pipelines. + +```typescript +import { CatalogItemSchema } from '@lucid-agents/catalog'; + +const result = CatalogItemSchema.safeParse({ + key: 'test', + name: 'Test Item', + price: '0.01', +}); +``` + +### `CatalogItem` + +TypeScript type inferred from `CatalogItemSchema`: + +```typescript +type CatalogItem = { + key: string; + name: string; + description?: string; + price?: string | { invoke?: string; stream?: string }; + network?: string; + metadata?: Record; +}; +``` + +## Integration Examples + +### With x402 Payments + +Combine catalog routes with x402 payment gating: + +```typescript +import { createAgent } from '@lucid-agents/core'; +import { http } from '@lucid-agents/http'; +import { payments, paymentsFromEnv } from '@lucid-agents/payments'; +import { catalog } from '@lucid-agents/catalog'; +import { createAgentApp } from '@lucid-agents/hono'; + +const agent = await createAgent({ + name: 'paid-catalog-agent', + version: '1.0.0', + description: 'Catalog agent with x402 payments', +}) + .use(http()) + .use(payments({ config: paymentsFromEnv() })) + .use(catalog({ + file: './products.yaml', + handlerFactory: (item) => async ({ input }) => { + return { output: { product: item.name, data: 'processed' } }; + }, + })) + .build(); + +const { app } = await createAgentApp(agent); +export default app; +``` + +Each catalog item with a `price` field will automatically require x402 payment. + +### With MPP (Machine Payments Protocol) + +```typescript +import { createAgent } from '@lucid-agents/core'; +import { http } from '@lucid-agents/http'; +import { mpp, tempo } from '@lucid-agents/mpp'; +import { catalog } from '@lucid-agents/catalog'; +import { createAgentApp } from '@lucid-agents/hono'; + +const agent = await createAgent({ + name: 'mpp-catalog-agent', + version: '1.0.0', + description: 'Catalog agent with MPP payments', +}) + .use(http()) + .use(mpp({ + config: { + methods: [tempo.server({ currency: '0x...', recipient: '0x...' })], + currency: 'usd', + }, + })) + .use(catalog({ + file: './services.yaml', + network: 'eip155:8453', + handlerFactory: (item) => async ({ input }) => { + return { output: { service: item.name } }; + }, + })) + .build(); + +const { app } = await createAgentApp(agent); +export default app; +``` + +See `packages/examples/src/catalog/catalog-mpp-store.ts` for a full working example. + +### Custom Handler Factory + +The handler factory receives the full `CatalogItem`, giving you access to metadata for dynamic behavior: + +```typescript +import type { CatalogItem } from '@lucid-agents/catalog'; + +const handlerFactory = (item: CatalogItem) => async ({ input }: any) => { + const model = item.metadata?.model as string ?? 'gpt-4o-mini'; + const category = item.metadata?.category as string ?? 'general'; + + // Route to different backends based on metadata + const result = await processWithModel(model, category, input); + + return { output: result }; +}; + +agent.use(catalog({ + file: './products.yaml', + handlerFactory, +})); +``` + +### Key Prefix for Namespacing + +Use `keyPrefix` to namespace catalog routes, avoiding collisions when loading multiple catalogs: + +```typescript +const agent = await createAgent({ + name: 'multi-catalog-agent', + version: '1.0.0', +}) + .use(http()) + .use(catalog({ file: './nlp-services.yaml', keyPrefix: 'nlp/' })) + .use(catalog({ file: './vision-services.csv', keyPrefix: 'vision/' })) + .build(); +``` + +This produces routes like `nlp/sentiment`, `nlp/translate`, `vision/classify`, etc. + +## Network Support + +Network identifiers follow the [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) format. Common values: + +| Network | CAIP-2 ID | +|---------|-----------| +| Base | `eip155:8453` | +| Base Sepolia | `eip155:84532` | +| Ethereum | `eip155:1` | +| Solana Mainnet | `solana:mainnet` | +| Solana Devnet | `solana:devnet` | + +Set a default network for all items via the `network` option, or override per item in your catalog file. + +## Related Packages + +| Package | Description | +|---------|-------------| +| [`@lucid-agents/core`](../core) | Core runtime with extension system and `createAgent()` | +| [`@lucid-agents/http`](../http) | HTTP extension for request handling | +| [`@lucid-agents/payments`](../payments) | x402 payment tracking and policy enforcement | +| [`@lucid-agents/mpp`](../mpp) | Machine Payments Protocol integration | +| [`@lucid-agents/hono`](../hono) | Hono adapter for serving agents over HTTP | diff --git a/packages/examples/package.json b/packages/examples/package.json index 942c26f7..1825ab49 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -18,6 +18,7 @@ "@lucid-agents/a2a": "workspace:*", "@lucid-agents/analytics": "workspace:*", "@lucid-agents/ap2": "workspace:*", + "@lucid-agents/catalog": "workspace:*", "@lucid-agents/core": "workspace:*", "@lucid-agents/hono": "workspace:*", "@lucid-agents/http": "workspace:*", diff --git a/packages/examples/src/catalog/catalog-mpp-store.ts b/packages/examples/src/catalog/catalog-mpp-store.ts new file mode 100644 index 00000000..0977f00f --- /dev/null +++ b/packages/examples/src/catalog/catalog-mpp-store.ts @@ -0,0 +1,127 @@ +import { createAgent } from '@lucid-agents/core'; +import { createAgentApp } from '@lucid-agents/hono'; +import { http } from '@lucid-agents/http'; +import { mpp, tempo } from '@lucid-agents/mpp'; +import { catalog, type CatalogItem } from '@lucid-agents/catalog'; +import { join } from 'path'; + +/** + * Catalog + MPP Store Agent + * + * Demonstrates how to generate hundreds of paid entrypoint routes from a single + * YAML catalog file, with payments enforced via Machine Payments Protocol (MPP). + * + * Instead of writing 10+ addEntrypoint() calls manually, the catalog extension + * reads products.yaml and auto-registers each product as a paid route. + * + * Run: bun run packages/examples/src/catalog/catalog-mpp-store.ts + * + * Environment variables: + * MPP_TEMPO_CURRENCY - Token address (default: pathUSD contract) + * MPP_TEMPO_RECIPIENT - Recipient wallet address (default: dev wallet) + * PORT - Server port (default: 3000) + */ + +// ─── Handler Factory ───────────────────────────────────────────── +// Each catalog item gets its own handler via this factory. +// In production, you'd route to actual AI models or services. + +const handlerFactory = (item: CatalogItem) => { + return async (ctx: { input: { params?: Record } }) => { + return { + output: { + product: item.key, + name: item.name, + description: item.description ?? 'No description', + price: item.price ?? 'free', + metadata: item.metadata ?? {}, + params: ctx.input.params ?? {}, + timestamp: new Date().toISOString(), + }, + }; + }; +}; + +// ─── Agent Setup ───────────────────────────────────────────────── + +const catalogFile = join(import.meta.dir, 'products.yaml'); + +const agent = await createAgent({ + name: 'catalog-mpp-store', + version: '1.0.0', + description: 'AI service marketplace powered by YAML catalog + MPP payments', +}) + .use(http()) + .use( + mpp({ + config: { + methods: [ + tempo.server({ + currency: + process.env.MPP_TEMPO_CURRENCY ?? + '0x20c0000000000000000000000000000000000000', // pathUSD + recipient: + process.env.MPP_TEMPO_RECIPIENT ?? + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // dev wallet + }), + ], + currency: 'usd', + defaultIntent: 'charge', + }, + }) + ) + .use( + catalog({ + file: catalogFile, + keyPrefix: 'store/', + handlerFactory, + }) + ) + .build(); + +// ─── Access Catalog at Runtime ─────────────────────────────────── +// The catalog runtime exposes parsed items for introspection. + +const items = (agent as any).catalog?.items ?? []; +console.log(`\nLoaded ${items.length} products from catalog\n`); + +// ─── Create Hono App ───────────────────────────────────────────── + +const { app } = await createAgentApp(agent); + +// ─── Start Server ──────────────────────────────────────────────── + +const port = Number(process.env.PORT ?? 3000); + +const server = Bun.serve({ + port, + fetch: app.fetch, +}); + +console.log( + `Catalog MPP Store ready at http://${server.hostname}:${server.port}\n` +); +console.log('Endpoints:'); +console.log(' GET / -> Landing page'); +console.log(' GET /.well-known/agent.json -> Agent manifest'); +console.log(''); + +// List all catalog-generated routes with prices +for (const item of items) { + const key = `store/${item.key}`; + const price = + typeof item.price === 'object' + ? `invoke: $${item.price.invoke ?? '0'}, stream: $${item.price.stream ?? '0'}` + : item.price + ? `$${item.price}` + : 'free'; + const tier = item.metadata?.tier ?? '-'; + console.log( + ` POST /entrypoints/${key}/invoke -> ${item.name} (${price}) [${tier}]` + ); +} + +console.log( + '\nPayment: Machine Payments Protocol (HTTP 402 + WWW-Authenticate)' +); +console.log('Methods: tempo (stablecoin)\n'); diff --git a/packages/examples/src/catalog/products.csv b/packages/examples/src/catalog/products.csv new file mode 100644 index 00000000..5f7d12cf --- /dev/null +++ b/packages/examples/src/catalog/products.csv @@ -0,0 +1,6 @@ +key,name,description,price,network,meta_tier,meta_category,meta_rateLimit +ocr,OCR Scan,Extract text from images using OCR,0.50,,standard,vision,200 +embed,Text Embedding,Generate vector embeddings for text,0.02,,basic,ml,2000 +classify,Text Classifier,Classify text into categories,0.10,,basic,nlp,500 +rag-query,RAG Query,Retrieval-augmented generation query,1.00,,premium,search,100 +sol-verify,Solana Verify,Verify Solana transaction signatures,0.05,solana:devnet,standard,blockchain,1000 diff --git a/packages/examples/src/catalog/products.yaml b/packages/examples/src/catalog/products.yaml new file mode 100644 index 00000000..0e0696b7 --- /dev/null +++ b/packages/examples/src/catalog/products.yaml @@ -0,0 +1,106 @@ +# AI Service Product Catalog +# +# Each product becomes an entrypoint route on the agent. +# Prices are in USD. Payment is enforced via MPP (HTTP 402). +# +# Supported fields: +# key - unique route identifier (required) +# name - display name (required) +# description - human-readable description +# price - flat string ("1.00") or { invoke, stream } object +# network - CAIP-2 network identifier +# metadata - arbitrary key-value pairs + +products: + # ── Free tier ──────────────────────────────────────────────────── + - key: health + name: Health Check + description: Free health check endpoint + metadata: + tier: free + category: system + + # ── Basic tier ($0.01 - $0.10) ────────────────────────────────── + - key: text-stats + name: Text Statistics + description: Word count, character count, and reading time estimate + price: "0.01" + metadata: + tier: basic + category: text + rateLimit: 1000 + + - key: sentiment + name: Sentiment Analysis + description: Analyze text sentiment (positive, negative, neutral) + price: "0.05" + metadata: + tier: basic + category: nlp + rateLimit: 500 + + - key: translate + name: Quick Translate + description: Translate short text between languages + price: "0.10" + metadata: + tier: basic + category: nlp + rateLimit: 200 + + # ── Standard tier ($0.25 - $1.00) ────────────────────────────── + - key: summarize + name: Text Summarizer + description: Summarize long-form content into key points + price: + invoke: "0.50" + stream: "0.10" + metadata: + tier: standard + category: text + + - key: code-review + name: Code Review + description: Automated code review with suggestions + price: "1.00" + metadata: + tier: standard + category: code + rateLimit: 100 + + # ── Premium tier ($2.00 - $5.00) ──────────────────────────────── + - key: image-describe + name: Image Description + description: Generate detailed descriptions of images + price: "2.00" + metadata: + tier: premium + category: vision + + - key: document-extract + name: Document Extraction + description: Extract structured data from PDFs and documents + price: "3.00" + metadata: + tier: premium + category: extraction + + - key: generate-report + name: Report Generator + description: Generate comprehensive analysis reports + price: + invoke: "5.00" + stream: "1.00" + metadata: + tier: premium + category: analysis + + # ── Cross-chain product ───────────────────────────────────────── + - key: solana-nft-metadata + name: Solana NFT Metadata + description: Fetch and analyze Solana NFT metadata + price: "0.25" + network: "solana:devnet" + metadata: + tier: standard + category: blockchain From ea5abb7a040f25c952efbc70455619d887fa6da6 Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Sat, 21 Mar 2026 23:04:55 +1100 Subject: [PATCH 4/6] docs(site): add catalog package to lucid-docs site - Add packages/catalog.mdx with full documentation: YAML/CSV format, configuration, handler factories, payment integration, API reference - Add catalog to sidebar navigation (Extensions section) - Add catalog card to packages index page - Update architecture diagram to include catalog extension --- lucid-docs/content/docs/packages/catalog.mdx | 369 +++++++++++++++++++ lucid-docs/content/docs/packages/index.mdx | 7 +- lucid-docs/content/docs/packages/meta.json | 1 + 3 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 lucid-docs/content/docs/packages/catalog.mdx diff --git a/lucid-docs/content/docs/packages/catalog.mdx b/lucid-docs/content/docs/packages/catalog.mdx new file mode 100644 index 00000000..2271b1fd --- /dev/null +++ b/lucid-docs/content/docs/packages/catalog.mdx @@ -0,0 +1,369 @@ +--- +title: '@lucid-agents/catalog' +description: YAML/CSV catalog-driven route generation for large product catalogs. +icon: List +--- + +The catalog extension generates entrypoint routes from YAML or CSV product catalog files. Instead of writing hundreds of `addEntrypoint()` calls, define your catalog in a single file and let the extension register all routes at build time. + +## Installation + +```bash +bun add @lucid-agents/catalog +``` + +## Basic usage + +Define a product catalog in YAML: + +```yaml +# products.yaml +products: + - key: sentiment + name: Sentiment Analysis + description: Analyze text sentiment + price: "0.05" + - key: summarize + name: Text Summarizer + description: Summarize long-form content + price: + invoke: "0.50" + stream: "0.10" + - key: health + name: Health Check + description: Free health endpoint +``` + +Wire it into your agent: + +```typescript +import { createAgent } from '@lucid-agents/core'; +import { http } from '@lucid-agents/http'; +import { catalog } from '@lucid-agents/catalog'; +import { createAgentApp } from '@lucid-agents/hono'; + +const agent = await createAgent({ + name: 'store-agent', + version: '1.0.0', +}) + .use(http()) + .use(catalog({ file: './products.yaml' })) + .build(); + +const { app } = await createAgentApp(agent); +``` + +Each item in the YAML file becomes a discoverable, invocable entrypoint on your agent. Items with a `price` field will require payment when combined with the payments or MPP extensions. + +## YAML format + +YAML catalogs support a `products` wrapper object or a top-level array: + +```yaml +products: + - key: translate + name: Quick Translate + description: Translate text between languages + price: "0.10" + network: "eip155:8453" + metadata: + category: nlp + rateLimit: 200 +``` + +### Supported fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `key` | `string` | Yes | Unique route identifier. Becomes the entrypoint key. | +| `name` | `string` | Yes | Human-readable display name. | +| `description` | `string` | No | Description shown in the agent manifest. | +| `price` | `string` or `object` | No | Flat USD price, or `{ invoke, stream }` for per-mode pricing. | +| `network` | `string` | No | CAIP-2 network identifier (e.g., `eip155:8453`). | +| `metadata` | `object` | No | Arbitrary key-value pairs attached to the entrypoint. | + +### Price variants + +Flat price (same for invoke and stream): + +```yaml +- key: summarize + name: Summarize + price: "0.50" +``` + +Separate invoke and stream prices: + +```yaml +- key: generate + name: Generate Text + price: + invoke: "1.00" + stream: "0.20" +``` + +No price (free entrypoint): + +```yaml +- key: health + name: Health Check +``` + +## CSV format + +CSV catalogs use a header row. Metadata columns use the `meta_` prefix convention: + +```csv +key,name,description,price,network,meta_category,meta_tier +ocr,OCR Scan,Extract text from images,0.50,,vision,standard +embed,Text Embedding,Generate vector embeddings,0.02,,ml,basic +classify,Text Classifier,Classify text,0.10,,nlp,basic +``` + +| Column | Required | Description | +|--------|----------|-------------| +| `key` | Yes | Unique route identifier. | +| `name` | Yes | Display name. | +| `description` | No | Route description. | +| `price` | No | USD price string. Empty = free. | +| `network` | No | CAIP-2 network identifier. | +| `meta_*` | No | Collected into `metadata` with prefix stripped. `meta_category` becomes `metadata.category`. | + + +CSV does not support the `{ invoke, stream }` price object syntax. Use YAML if you need per-mode pricing. + + +## Configuration + +### CatalogExtensionOptions + +```typescript +type CatalogExtensionOptions = { + file: string; // Path to .yaml, .yml, or .csv file + keyPrefix?: string; // Prefix for all item keys + network?: string; // Default CAIP-2 network + handlerFactory?: HandlerFactory; // Generate handlers from items + inputSchema?: z.ZodTypeAny; // Custom input schema for all routes +}; +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `file` | (required) | Path to the catalog file. Supports `.yaml`, `.yml`, and `.csv`. | +| `keyPrefix` | `undefined` | Prepended to every item key. `"store/"` turns `"widget"` into `"store/widget"`. | +| `network` | `undefined` | Default network for items that don't specify their own. | +| `handlerFactory` | `undefined` | Function receiving a `CatalogItem`, returning a handler function. | +| `inputSchema` | `z.object({ params: z.record(...).optional() })` | Zod schema applied as input to all generated entrypoints. | + +### Handler factory + +The handler factory generates a route handler for each catalog item. This is where you connect catalog items to actual business logic: + +```typescript +import type { CatalogItem } from '@lucid-agents/catalog'; + +const handlerFactory = (item: CatalogItem) => { + return async (ctx: { input: { params?: Record } }) => { + const model = item.metadata?.model as string ?? 'gpt-4o-mini'; + + // Route to different backends based on catalog metadata + const result = await callModel(model, ctx.input); + + return { + output: { + product: item.key, + result, + }, + }; + }; +}; + +agent.use(catalog({ + file: './products.yaml', + handlerFactory, +})); +``` + +### Key prefix + +Use `keyPrefix` to namespace routes, especially when loading multiple catalogs: + +```typescript +const agent = await createAgent({ name: 'multi-catalog', version: '1.0.0' }) + .use(http()) + .use(catalog({ file: './nlp-services.yaml', keyPrefix: 'nlp/' })) + .use(catalog({ file: './vision-services.csv', keyPrefix: 'vision/' })) + .build(); +``` + +This produces routes like `nlp/sentiment`, `nlp/translate`, `vision/classify`. + +## Integration with payments + +### With x402 + +Combine with the payments extension for x402 payment gating. Any catalog item with a `price` field will automatically require payment: + +```typescript +import { payments, paymentsFromEnv } from '@lucid-agents/payments'; +import { catalog } from '@lucid-agents/catalog'; + +const agent = await createAgent({ + name: 'paid-catalog', + version: '1.0.0', +}) + .use(http()) + .use(payments({ config: paymentsFromEnv() })) + .use(catalog({ + file: './products.yaml', + handlerFactory: (item) => async ({ input }) => ({ + output: { product: item.name, data: 'processed' }, + }), + })) + .build(); +``` + +### With MPP + +Combine with the MPP extension for multi-method payments (Tempo, Stripe, Lightning): + +```typescript +import { mpp, tempo } from '@lucid-agents/mpp'; +import { catalog } from '@lucid-agents/catalog'; + +const agent = await createAgent({ + name: 'mpp-catalog', + version: '1.0.0', +}) + .use(http()) + .use(mpp({ + config: { + methods: [tempo.server({ currency: '0x...', recipient: '0x...' })], + currency: 'usd', + }, + })) + .use(catalog({ + file: './products.yaml', + keyPrefix: 'store/', + handlerFactory: (item) => async ({ input }) => ({ + output: { product: item.key, price: item.price }, + }), + })) + .build(); +``` + +Both x402 and MPP can coexist on the same agent alongside catalog. + +## Runtime access + +After building, the catalog items are available on the runtime for introspection: + +```typescript +const agent = await createAgent({ ... }) + .use(catalog({ file: './products.yaml' })) + .build(); + +// Access parsed catalog items +const items = (agent as any).catalog.items; +console.log(`Loaded ${items.length} products`); + +for (const item of items) { + console.log(`${item.key}: ${item.name} — $${item.price ?? 'free'}`); +} +``` + +## API reference + +### parseCatalogYaml(content) + +Parses a YAML string into validated `CatalogItem[]`. Accepts either a top-level array or `{ products: [...] }` wrapper. + +```typescript +import { parseCatalogYaml } from '@lucid-agents/catalog'; + +const items = parseCatalogYaml(yamlString); +``` + +Throws if any item fails validation against `CatalogItemSchema`. + +### parseCatalogCsv(content) + +Parses a CSV string into validated `CatalogItem[]`. Requires a `key` column. Columns prefixed with `meta_` are collected into `metadata`. + +```typescript +import { parseCatalogCsv } from '@lucid-agents/catalog'; + +const items = parseCatalogCsv(csvString); +``` + +### generateEntrypoints(items, options?) + +Converts `CatalogItem[]` into `EntrypointDef[]` ready for registration: + +```typescript +import { generateEntrypoints } from '@lucid-agents/catalog'; + +const entrypoints = generateEntrypoints(items, { + keyPrefix: 'api/', + network: 'eip155:8453', + handlerFactory: (item) => async ({ input }) => ({ + output: { result: item.name }, + }), +}); +``` + +### CatalogItemSchema + +Zod schema for validating catalog items. Useful for custom parsing pipelines: + +```typescript +import { CatalogItemSchema } from '@lucid-agents/catalog'; + +const result = CatalogItemSchema.safeParse({ + key: 'test', + name: 'Test Item', + price: '0.01', +}); +``` + +## Network support + +Network identifiers use [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) format: + +| Network | CAIP-2 ID | +|---------|-----------| +| Base | `eip155:8453` | +| Base Sepolia | `eip155:84532` | +| Ethereum | `eip155:1` | +| Solana Mainnet | `solana:mainnet` | +| Solana Devnet | `solana:devnet` | + +Set a default via the `network` option, or override per item in your catalog file. Item-level network always takes precedence over the global default. + +## Exports + +```typescript +// Extension +export { catalog } from '@lucid-agents/catalog'; + +// Parsers +export { parseCatalogYaml, parseCatalogCsv } from '@lucid-agents/catalog'; + +// Entrypoint generation +export { generateEntrypoints } from '@lucid-agents/catalog'; + +// Schema & types +export { + CatalogItemSchema, + type CatalogItem, + type CatalogConfig, + type CatalogExtensionOptions, +} from '@lucid-agents/catalog'; +``` + +## Related + +- [Payments Extension](/docs/packages/payments) — x402 payment protocol (USDC) +- [MPP Extension](/docs/packages/mpp) — Machine Payments Protocol (multi-method) +- [HTTP Extension](/docs/packages/http) — HTTP request/response handling +- [Core](/docs/packages/core) — Agent runtime and extension system diff --git a/lucid-docs/content/docs/packages/index.mdx b/lucid-docs/content/docs/packages/index.mdx index 8284100c..c2a0d285 100644 --- a/lucid-docs/content/docs/packages/index.mdx +++ b/lucid-docs/content/docs/packages/index.mdx @@ -53,6 +53,11 @@ Optional capabilities you can add to your agent: description="Machine Payments Protocol — multi-method payments via HTTP 402" href="/docs/packages/mpp" /> + Date: Sat, 21 Mar 2026 23:12:51 +1100 Subject: [PATCH 5/6] fix(examples): fix lint errors in catalog-mpp-store example - Sort imports alphabetically (simple-import-sort/imports) - Replace `as any` cast with typed assertion for catalog runtime access --- packages/examples/src/catalog/catalog-mpp-store.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/examples/src/catalog/catalog-mpp-store.ts b/packages/examples/src/catalog/catalog-mpp-store.ts index 0977f00f..b1a0b81a 100644 --- a/packages/examples/src/catalog/catalog-mpp-store.ts +++ b/packages/examples/src/catalog/catalog-mpp-store.ts @@ -1,8 +1,8 @@ +import { catalog, type CatalogItem } from '@lucid-agents/catalog'; import { createAgent } from '@lucid-agents/core'; import { createAgentApp } from '@lucid-agents/hono'; import { http } from '@lucid-agents/http'; import { mpp, tempo } from '@lucid-agents/mpp'; -import { catalog, type CatalogItem } from '@lucid-agents/catalog'; import { join } from 'path'; /** @@ -82,7 +82,8 @@ const agent = await createAgent({ // ─── Access Catalog at Runtime ─────────────────────────────────── // The catalog runtime exposes parsed items for introspection. -const items = (agent as any).catalog?.items ?? []; +const items: CatalogItem[] = + (agent as unknown as { catalog?: { items: CatalogItem[] } }).catalog?.items ?? []; console.log(`\nLoaded ${items.length} products from catalog\n`); // ─── Create Hono App ───────────────────────────────────────────── From 84d5d3887bc100ceea6eca06b471ed784720504a Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Sun, 22 Mar 2026 07:28:11 +1100 Subject: [PATCH 6/6] fix(examples): fix formatting in catalog example files Run prettier on catalog-mpp-store.ts and products.yaml. --- .../examples/src/catalog/catalog-mpp-store.ts | 3 ++- packages/examples/src/catalog/products.yaml | 24 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/examples/src/catalog/catalog-mpp-store.ts b/packages/examples/src/catalog/catalog-mpp-store.ts index b1a0b81a..adb04d6f 100644 --- a/packages/examples/src/catalog/catalog-mpp-store.ts +++ b/packages/examples/src/catalog/catalog-mpp-store.ts @@ -83,7 +83,8 @@ const agent = await createAgent({ // The catalog runtime exposes parsed items for introspection. const items: CatalogItem[] = - (agent as unknown as { catalog?: { items: CatalogItem[] } }).catalog?.items ?? []; + (agent as unknown as { catalog?: { items: CatalogItem[] } }).catalog?.items ?? + []; console.log(`\nLoaded ${items.length} products from catalog\n`); // ─── Create Hono App ───────────────────────────────────────────── diff --git a/packages/examples/src/catalog/products.yaml b/packages/examples/src/catalog/products.yaml index 0e0696b7..32fdcb2a 100644 --- a/packages/examples/src/catalog/products.yaml +++ b/packages/examples/src/catalog/products.yaml @@ -24,7 +24,7 @@ products: - key: text-stats name: Text Statistics description: Word count, character count, and reading time estimate - price: "0.01" + price: '0.01' metadata: tier: basic category: text @@ -33,7 +33,7 @@ products: - key: sentiment name: Sentiment Analysis description: Analyze text sentiment (positive, negative, neutral) - price: "0.05" + price: '0.05' metadata: tier: basic category: nlp @@ -42,7 +42,7 @@ products: - key: translate name: Quick Translate description: Translate short text between languages - price: "0.10" + price: '0.10' metadata: tier: basic category: nlp @@ -53,8 +53,8 @@ products: name: Text Summarizer description: Summarize long-form content into key points price: - invoke: "0.50" - stream: "0.10" + invoke: '0.50' + stream: '0.10' metadata: tier: standard category: text @@ -62,7 +62,7 @@ products: - key: code-review name: Code Review description: Automated code review with suggestions - price: "1.00" + price: '1.00' metadata: tier: standard category: code @@ -72,7 +72,7 @@ products: - key: image-describe name: Image Description description: Generate detailed descriptions of images - price: "2.00" + price: '2.00' metadata: tier: premium category: vision @@ -80,7 +80,7 @@ products: - key: document-extract name: Document Extraction description: Extract structured data from PDFs and documents - price: "3.00" + price: '3.00' metadata: tier: premium category: extraction @@ -89,8 +89,8 @@ products: name: Report Generator description: Generate comprehensive analysis reports price: - invoke: "5.00" - stream: "1.00" + invoke: '5.00' + stream: '1.00' metadata: tier: premium category: analysis @@ -99,8 +99,8 @@ products: - key: solana-nft-metadata name: Solana NFT Metadata description: Fetch and analyze Solana NFT metadata - price: "0.25" - network: "solana:devnet" + price: '0.25' + network: 'solana:devnet' metadata: tier: standard category: blockchain