Skip to content

feat(catalog): YAML/CSV catalog-driven route generation#1617

Merged
ponderingdemocritus merged 6 commits into
masterfrom
ponderingdemocritus/catalog-routes
Mar 21, 2026
Merged

feat(catalog): YAML/CSV catalog-driven route generation#1617
ponderingdemocritus merged 6 commits into
masterfrom
ponderingdemocritus/catalog-routes

Conversation

@ponderingdemocritus

Copy link
Copy Markdown
Contributor

Summary

  • Adds new @lucid-agents/catalog package that generates entrypoint routes from YAML or CSV product catalog files
  • Eliminates manual route definition for large product catalogs (e.g. 400 products = 1 YAML file instead of 400 addEntrypoint() calls)
  • Works as a standard Lucid Agents extension — composable with x402 payments, MPP, identity, and all other extensions

Usage

# products.yaml
products:
  - key: premium-api
    name: Premium API
    description: AI-powered data endpoint
    price: "1.00"
  - key: streaming-feed
    name: Streaming Feed
    price:
      invoke: "5.00"
      stream: "0.50"
    network: base-sepolia
import { catalog } from '@lucid-agents/catalog';

const agent = await createAgent({ name: 'store', version: '1.0.0' })
  .use(http())
  .use(payments({ config: paymentsFromEnv() }))
  .use(catalog({
    file: './products.yaml',
    keyPrefix: 'store/',
    handlerFactory: (item) => async (ctx) => ({
      output: { product: item.key, price: item.price }
    }),
  }))
  .build();

Also supports CSV with meta_ prefixed columns for metadata:

key,name,description,price,meta_category,meta_tier
premium,Premium,Top tier,10.00,ai,premium

What's included

File Purpose
types.ts CatalogItemSchema (Zod), CatalogItem, config types
parser.ts parseCatalogYaml() + parseCatalogCsv()
entrypoints.ts generateEntrypoints() — maps items to EntrypointDef[]
extension.ts catalog() extension factory with onBuild hook

Test plan

  • 39 tests passing (bun test in packages/catalog)
  • Schema validation (required fields, optional fields, pricing formats)
  • YAML parsing (products wrapper, flat array, metadata, network, invoke/stream pricing)
  • CSV parsing (basic, quoted fields, empty price, network, meta_ columns)
  • Entrypoint generation (pricing, metadata passthrough, key prefix, network override, handler factory, input schema)
  • Extension integration (build, onBuild, file loading, error handling)
  • Package builds with tsup (bun run build)

🤖 Generated with Claude Code

…eneration

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Mar 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
lucid-agents 84d5d38 Commit Preview URL

Branch Preview URL
Mar 21 2026, 08:29 PM

@greptile-apps

greptile-apps Bot commented Mar 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a new @lucid-agents/catalog package that eliminates manual addEntrypoint() calls for large product catalogs by generating routes from a YAML or CSV file. The implementation is well-structured and fits cleanly into the existing extension system.

Key findings:

  • P1 — CSV catalog.items always empty in runtime: When a CSV file is used, build() captures a reference to an empty catalogItems array and returns it in the runtime slice. onBuild later reassigns catalogItems to the parsed result, but this does not update the already-returned catalog.items reference. Any consumer that inspects runtime.catalog.items after build will always see [] for CSV-backed catalogs. Entrypoint registration itself works correctly, but the runtime inspection API is silently broken for CSV. Making parseCatalogCsv synchronous (it already uses csv-parse/sync and has no await) and calling it directly in build() would eliminate both this bug and the deferred-parse complexity.
  • P2 — generateEntrypoints returns any[]: Should return EntrypointDef[] from @lucid-agents/types per the project style guide ("Avoid any").
  • P2 — Extension does not implement Extension<R>: The factory's return type is manually typed with any parameters. It should implement Extension<{ catalog?: CatalogRuntime }> from @lucid-agents/types, using BuildContext and AgentRuntime for the hook parameters.
  • P2 — parseCatalogCsv is unnecessarily async: The function uses csv-parse/sync with no await — it can be made synchronous, which also resolves the deferred-parse pattern driving the CSV items bug above.
  • Missing release artifacts: Per AGENTS.md, PRs adding new SDK surface area require (1) a changeset file in .changeset/, (2) an E2E smoke test in packages/examples/src/__tests__/smoke.test.ts, and (3) updated AGENTS.md / package README.md. None of these are present in this PR.

Confidence Score: 3/5

  • Safe to merge after fixing the CSV catalog.items bug and adding the missing release artifacts required by AGENTS.md.
  • The core entrypoint-registration path works correctly for both YAML and CSV, and the 39-test suite is thorough. However, there is one concrete P1 logic bug (CSV catalog.items silently empty in the runtime slice) that will produce wrong results for any consumer inspecting catalog items at runtime, plus three missing required artifacts (changeset, smoke test, README/AGENTS.md update) that the project's own AGENTS.md marks as non-optional for new packages. Resolving the items bug is straightforward (make parseCatalogCsv synchronous) but needs to happen before merge.
  • packages/catalog/src/extension.ts — CSV catalog.items bug; packages/catalog/src/parser.ts — unnecessary async on parseCatalogCsv; packages/catalog/src/entrypoints.ts — any[] return type

Important Files Changed

Filename Overview
packages/catalog/src/extension.ts Core extension factory — contains a P1 logic bug where catalog.items is always empty for CSV-backed catalogs due to variable reassignment vs. mutation, and uses any instead of the typed Extension<R> interface.
packages/catalog/src/entrypoints.ts Generates EntrypointDef objects from catalog items; logic is correct but returns any[] instead of EntrypointDef[], losing downstream type safety.
packages/catalog/src/parser.ts YAML/CSV parsers with Zod validation; logic is sound. parseCatalogCsv is unnecessarily async (uses csv-parse/sync with no await).
packages/catalog/src/types.ts Clean Zod schema and TypeScript types for catalog items and extension options; no issues found.
packages/catalog/src/tests/catalog.test.ts Good coverage of 39 tests across all modules; CSV tests don't assert catalog.items after build, missing the items-empty bug.

Sequence Diagram

sequenceDiagram
    participant User
    participant AgentBuilder
    participant CatalogExtension
    participant Parser
    participant EntrypointRegistry

    User->>AgentBuilder: createAgent().use(catalog({ file })).build()
    AgentBuilder->>CatalogExtension: build(ctx)
    alt YAML / YML file
        CatalogExtension->>Parser: parseCatalogYaml(content)
        Parser-->>CatalogExtension: CatalogItem[]
        CatalogExtension-->>AgentBuilder: { catalog: { items: CatalogItem[] } }
    else CSV file
        CatalogExtension->>Parser: parseCatalogCsv(content) [async]
        Note over CatalogExtension: pendingCsvParse set, catalogItems still []
        CatalogExtension-->>AgentBuilder: { catalog: { items: [] } } ⚠️ always empty
    end
    AgentBuilder->>CatalogExtension: onBuild(runtime)
    alt CSV pending
        CatalogExtension->>Parser: await pendingCsvParse
        Parser-->>CatalogExtension: CatalogItem[] (reassigns variable, not catalog.items)
    end
    CatalogExtension->>CatalogExtension: generateEntrypoints(catalogItems, options)
    loop for each EntrypointDef
        CatalogExtension->>EntrypointRegistry: runtime.entrypoints.add(ep)
    end
    EntrypointRegistry-->>User: entrypoints registered ✓
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/catalog/src/extension.ts
Line: 37-41

Comment:
**CSV `catalog.items` always empty in runtime slice**

When a CSV file is used, `build()` returns `{ catalog: { items: catalogItems } }` while `catalogItems` is still `[]` (CSV parsing is deferred). Later in `onBuild`, `catalogItems` is **reassigned** to the parsed array — but this doesn't update the already-returned `catalog.items` reference, which still points to the original empty array.

```typescript
// build() captures a reference to the current (empty) array
return {
  catalog: {
    items: catalogItems, // [] at this point for CSV
  },
};
```

Then in `onBuild`:
```typescript
catalogItems = await pendingCsvParse; // ← reassigns the variable, NOT the captured array
```

Any consumer that reads `runtime.catalog.items` after build will always see `[]` for CSV-based catalogs, even though entrypoints are correctly registered. The existing test suite confirms this gap — the CSV test only verifies that entrypoints are added, not that `catalog.items` is populated.

**Fix options:**
1. Mutate the existing array in `onBuild` instead of reassigning: `catalogItems.push(...(await pendingCsvParse))`, and initialise `catalogItems` above it.
2. Store the returned runtime object and update it in `onBuild`: `runtimeSlice.catalog!.items = catalogItems`.
3. Return the catalog runtime slice only from `onBuild` via a pattern that supports deferred population.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/catalog/src/entrypoints.ts
Line: 15-18

Comment:
**Return type should be `EntrypointDef[]` instead of `any[]`**

`generateEntrypoints` returns `any[]`, losing all type-safety for callers. `@lucid-agents/types` is a declared dependency and exports `EntrypointDef` — the project style guide in `AGENTS.md` explicitly calls out "Avoid `any`, prefer explicit types or `unknown`".

```suggestion
import type { EntrypointDef } from '@lucid-agents/types';

export function generateEntrypoints(
  items: CatalogItem[],
  options?: GenerateOptions,
): EntrypointDef[] {
```

The `entrypoint` object built inside the map can also be typed as `EntrypointDef` rather than `Record<string, unknown>`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/catalog/src/extension.ts
Line: 11-15

Comment:
**Extension does not implement the `Extension<R>` interface**

The return type of `catalog()` is manually typed as `{ name, build, onBuild? }` using `any` for both the `build` context and the `onBuild` runtime parameter. `@lucid-agents/types` exports the `Extension<R>` interface that all extensions should conform to — `BuildContext` and `AgentRuntime` are the correct types for those parameters.

Using `any` means TypeScript won't catch shape mismatches and the builder's type inference will degrade for consumers.

```typescript
import type { Extension, BuildContext } from '@lucid-agents/types';
import type { AgentRuntime } from '@lucid-agents/types';

export function catalog(
  options: CatalogExtensionOptions,
): Extension<{ catalog?: CatalogRuntime }> {
  // ...
  return {
    name: 'catalog',
    build(ctx: BuildContext): { catalog?: CatalogRuntime } { ... },
    async onBuild(runtime: AgentRuntime): Promise<void> { ... },
  };
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/catalog/src/parser.ts
Line: 37-39

Comment:
**`parseCatalogCsv` is unnecessarily `async`**

The function imports and calls `csv-parse/sync` which is fully synchronous — there is no `await` inside the function body. Marking it `async` wraps the return value in a redundant `Promise`, forces the CSV branch in `extension.ts` to defer to `onBuild` (contributing to the `catalog.items` bug above), and adds overhead with no benefit.

Consider removing `async` and returning `CatalogItem[]` directly, then calling it synchronously in `build()` alongside the YAML path.

```suggestion
export function parseCatalogCsv(
  content: string,
): CatalogItem[] {
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "feat(catalog): add @..."

Comment on lines +37 to +41
return {
catalog: {
items: catalogItems,
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 CSV catalog.items always empty in runtime slice

When a CSV file is used, build() returns { catalog: { items: catalogItems } } while catalogItems is still [] (CSV parsing is deferred). Later in onBuild, catalogItems is reassigned to the parsed array — but this doesn't update the already-returned catalog.items reference, which still points to the original empty array.

// build() captures a reference to the current (empty) array
return {
  catalog: {
    items: catalogItems, // [] at this point for CSV
  },
};

Then in onBuild:

catalogItems = await pendingCsvParse; // ← reassigns the variable, NOT the captured array

Any consumer that reads runtime.catalog.items after build will always see [] for CSV-based catalogs, even though entrypoints are correctly registered. The existing test suite confirms this gap — the CSV test only verifies that entrypoints are added, not that catalog.items is populated.

Fix options:

  1. Mutate the existing array in onBuild instead of reassigning: catalogItems.push(...(await pendingCsvParse)), and initialise catalogItems above it.
  2. Store the returned runtime object and update it in onBuild: runtimeSlice.catalog!.items = catalogItems.
  3. Return the catalog runtime slice only from onBuild via a pattern that supports deferred population.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/catalog/src/extension.ts
Line: 37-41

Comment:
**CSV `catalog.items` always empty in runtime slice**

When a CSV file is used, `build()` returns `{ catalog: { items: catalogItems } }` while `catalogItems` is still `[]` (CSV parsing is deferred). Later in `onBuild`, `catalogItems` is **reassigned** to the parsed array — but this doesn't update the already-returned `catalog.items` reference, which still points to the original empty array.

```typescript
// build() captures a reference to the current (empty) array
return {
  catalog: {
    items: catalogItems, // [] at this point for CSV
  },
};
```

Then in `onBuild`:
```typescript
catalogItems = await pendingCsvParse; // ← reassigns the variable, NOT the captured array
```

Any consumer that reads `runtime.catalog.items` after build will always see `[]` for CSV-based catalogs, even though entrypoints are correctly registered. The existing test suite confirms this gap — the CSV test only verifies that entrypoints are added, not that `catalog.items` is populated.

**Fix options:**
1. Mutate the existing array in `onBuild` instead of reassigning: `catalogItems.push(...(await pendingCsvParse))`, and initialise `catalogItems` above it.
2. Store the returned runtime object and update it in `onBuild`: `runtimeSlice.catalog!.items = catalogItems`.
3. Return the catalog runtime slice only from `onBuild` via a pattern that supports deferred population.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread packages/catalog/src/entrypoints.ts Outdated
Comment on lines +15 to +18
export function generateEntrypoints(
items: CatalogItem[],
options?: GenerateOptions,
): any[] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Return type should be EntrypointDef[] instead of any[]

generateEntrypoints returns any[], losing all type-safety for callers. @lucid-agents/types is a declared dependency and exports EntrypointDef — the project style guide in AGENTS.md explicitly calls out "Avoid any, prefer explicit types or unknown".

Suggested change
export function generateEntrypoints(
items: CatalogItem[],
options?: GenerateOptions,
): any[] {
import type { EntrypointDef } from '@lucid-agents/types';
export function generateEntrypoints(
items: CatalogItem[],
options?: GenerateOptions,
): EntrypointDef[] {

The entrypoint object built inside the map can also be typed as EntrypointDef rather than Record<string, unknown>.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/catalog/src/entrypoints.ts
Line: 15-18

Comment:
**Return type should be `EntrypointDef[]` instead of `any[]`**

`generateEntrypoints` returns `any[]`, losing all type-safety for callers. `@lucid-agents/types` is a declared dependency and exports `EntrypointDef` — the project style guide in `AGENTS.md` explicitly calls out "Avoid `any`, prefer explicit types or `unknown`".

```suggestion
import type { EntrypointDef } from '@lucid-agents/types';

export function generateEntrypoints(
  items: CatalogItem[],
  options?: GenerateOptions,
): EntrypointDef[] {
```

The `entrypoint` object built inside the map can also be typed as `EntrypointDef` rather than `Record<string, unknown>`.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread packages/catalog/src/extension.ts Outdated
Comment on lines +11 to +15
export function catalog(options: CatalogExtensionOptions): {
name: string;
build: (ctx: any) => { catalog?: CatalogRuntime };
onBuild?: (runtime: any) => Promise<void>;
} {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Extension does not implement the Extension<R> interface

The return type of catalog() is manually typed as { name, build, onBuild? } using any for both the build context and the onBuild runtime parameter. @lucid-agents/types exports the Extension<R> interface that all extensions should conform to — BuildContext and AgentRuntime are the correct types for those parameters.

Using any means TypeScript won't catch shape mismatches and the builder's type inference will degrade for consumers.

import type { Extension, BuildContext } from '@lucid-agents/types';
import type { AgentRuntime } from '@lucid-agents/types';

export function catalog(
  options: CatalogExtensionOptions,
): Extension<{ catalog?: CatalogRuntime }> {
  // ...
  return {
    name: 'catalog',
    build(ctx: BuildContext): { catalog?: CatalogRuntime } { ... },
    async onBuild(runtime: AgentRuntime): Promise<void> { ... },
  };
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/catalog/src/extension.ts
Line: 11-15

Comment:
**Extension does not implement the `Extension<R>` interface**

The return type of `catalog()` is manually typed as `{ name, build, onBuild? }` using `any` for both the `build` context and the `onBuild` runtime parameter. `@lucid-agents/types` exports the `Extension<R>` interface that all extensions should conform to — `BuildContext` and `AgentRuntime` are the correct types for those parameters.

Using `any` means TypeScript won't catch shape mismatches and the builder's type inference will degrade for consumers.

```typescript
import type { Extension, BuildContext } from '@lucid-agents/types';
import type { AgentRuntime } from '@lucid-agents/types';

export function catalog(
  options: CatalogExtensionOptions,
): Extension<{ catalog?: CatalogRuntime }> {
  // ...
  return {
    name: 'catalog',
    build(ctx: BuildContext): { catalog?: CatalogRuntime } { ... },
    async onBuild(runtime: AgentRuntime): Promise<void> { ... },
  };
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread packages/catalog/src/parser.ts Outdated
Comment on lines +37 to +39
export async function parseCatalogCsv(
content: string,
): Promise<CatalogItem[]> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 parseCatalogCsv is unnecessarily async

The function imports and calls csv-parse/sync which is fully synchronous — there is no await inside the function body. Marking it async wraps the return value in a redundant Promise, forces the CSV branch in extension.ts to defer to onBuild (contributing to the catalog.items bug above), and adds overhead with no benefit.

Consider removing async and returning CatalogItem[] directly, then calling it synchronously in build() alongside the YAML path.

Suggested change
export async function parseCatalogCsv(
content: string,
): Promise<CatalogItem[]> {
export function parseCatalogCsv(
content: string,
): CatalogItem[] {
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/catalog/src/parser.ts
Line: 37-39

Comment:
**`parseCatalogCsv` is unnecessarily `async`**

The function imports and calls `csv-parse/sync` which is fully synchronous — there is no `await` inside the function body. Marking it `async` wraps the return value in a redundant `Promise`, forces the CSV branch in `extension.ts` to defer to `onBuild` (contributing to the `catalog.items` bug above), and adds overhead with no benefit.

Consider removing `async` and returning `CatalogItem[]` directly, then calling it synchronously in `build()` alongside the YAML path.

```suggestion
export function parseCatalogCsv(
  content: string,
): CatalogItem[] {
```

How can I resolve this? If you propose a fix, please make it concise.

- 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<R> interface, BuildContext, AgentRuntime
- Type generateEntrypoints return as EntrypointDef[] instead of any[]
- Use CAIP-2 network format in tests (solana:devnet, eip155:84532)
- 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
- 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
- Sort imports alphabetically (simple-import-sort/imports)
- Replace `as any` cast with typed assertion for catalog runtime access
Run prettier on catalog-mpp-store.ts and products.yaml.
@ponderingdemocritus ponderingdemocritus merged commit a4e67f6 into master Mar 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant