npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reactionary/feeds

v0.10.57

Published

Reusable product-feed generation for Reactionary clients.

Downloads

3,525

Readme

@reactionary/feeds

Reusable product-feed generation for Reactionary clients.

The package keeps feed generation separate from protocol adapters. A host can provide a ready-made request-scoped Reactionary client and expose the same feed definitions through:

  • a mountable fetch-compatible HTTP handler,
  • a Node HTTP handler,
  • the reactionary-feeds CLI,
  • direct TypeScript APIs.

Built-in feed transformers

The default registry includes:

  • acp-product-feed — ACP Feed API (2026-04-17) Product records: products.jsonl for file ingestion by default (one product with its variants per line), or with format: 'json' the { "products": [...] } body of PATCH /feeds/{id}/products. Prices are integer minor units with upper-case currency codes. toACPFeedMetadata(feedId, feed) builds the matching metadata.json.
  • acp-feed-metadata — the ACP feed's metadata.json (id, target_country, updated_at) for file ingestion next to products.jsonl.
  • google-merchant-feed — Google Merchant RSS XML.
  • sitemap-feed — sitemap XML of product URLs.
  • pricerunner-feed — PriceRunner-style product XML.

All transformers use the shared normalized feed product model from ReactionaryFeedGenerator. XML outputs are generated through XML libraries (sitemap for sitemaps and xmlbuilder2 for RSS/product XML), not by manually concatenating escaped XML strings.

Default environment client builder

The package exports createReactionaryFeedClientFactoryFromEnv() for setups that should use the same environment-variable driven provider selection model as the MCP CLI, without depending on @reactionary/mcp.

The helper reads .env from the current working directory by default, or DOTENV_CONFIG_PATH when explicitly set. It supports the same provider switches and provider variables as the MCP command line package, for example:

ENABLED_MEDUSA=true
MEDUSA_API_URL=https://medusa.example
MEDUSA_PUBLISHABLE_KEY=pk_test_...

CLI config files may omit createClient; reactionary-feeds generate will then use createReactionaryFeedClientFactoryFromEnv() automatically.

The default client builder only enables the provider capabilities needed by feeds and sitemap sources. For full commerce providers this is product search, product details, price, inventory, categories, and stores where supported.

reactionary-feeds generate can write one or more outputs in a single source pass by repeating --output transformer=path. It writes a progress report to stderr by default, including processed products, pages, elapsed time, and estimated time to completion once the first search result exposes the total product count. Use --no-progress to disable it.

Products are normalized concurrently, up to 10 products at a time by default. Set productConcurrency in the config or pass --product-concurrency <count> to the CLI to tune this.

Use --testMode to cap source reads to at most 2 pages while smoke-testing a feed configuration.

You can still provide a project-specific client factory when needed:

import { createReactionaryFeedClientFactoryFromEnv } from '@reactionary/feeds';

export default {
  createClient: createReactionaryFeedClientFactoryFromEnv(),
  feeds: {
    // ...
  },
};

Required Reactionary operations

The generator validates the supplied client before use. The client must expose:

  • productSearch.queryByTerm
  • product.getBySKU
  • price.getCustomerPrice
  • price.getListPrice
  • inventory.getBySKU

price.getCustomerPrice is used as the effective feed price. price.getListPrice is exposed separately where the target feed supports it.

Feed definitions

import type { ReactionaryFeedDefinition } from '@reactionary/feeds';

export const finnishFeed: ReactionaryFeedDefinition = {
  languageContext: {
    locale: 'fi-FI',
    currencyCode: 'EUR',
  },
  search: {
    term: '',
    facets: [],
    filters: ['market:fi'],
    paginationOptions: {
      pageNumber: 1,
      pageSize: 50,
    },
  },
  productUrlBase: 'https://shop.example/{lang}/products/{slug}',
  sellerName: 'Example Shop',
};

The search value follows Reactionary core’s product-search identifier shape. languageContext is applied to the request context before the client is used.

Inventory requests use fulfillmentCenterKeys when configured on the feed. If the feed omits them, the generator uses defaultFulfillmentCenterKeys from the server or CLI config. The legacy singular fulfillmentCenterKey is still supported for existing configs.

export default {
  defaultFulfillmentCenterKeys: ['helsinki', 'tampere'],
  productConcurrency: 10,
  feeds: {
    finnish: {
      ...finnishFeed,
      fulfillmentCenterKeys: ['turku'],
    },
  },
};

productUrlBase can either be a normal base URL or a template supporting:

  • {lang} — the lowercase language part of languageContext.locale, e.g. fi-FI -> fi.
  • {slug} — the URL-encoded product slug.

HTTP usage

import { ReactionaryFeedServer } from '@reactionary/feeds';

const feeds = new ReactionaryFeedServer(createClient, {
  feeds: {
    finnish: finnishFeed,
  },
});

export const GET = (request: Request) => feeds.fetch(request);

Routes:

  • GET /feeds lists configured feed ids.
  • GET /feeds/transformers lists available transformers.
  • GET /feeds/{feedId}/outputs/{transformerId} streams generated output.
  • GET /sitemaps.xml returns a sitemap index when sitemaps are configured.
  • GET /sitemaps/{sourceId}.xml streams a configured sitemap source.

For Node HTTP:

import { createServer } from 'node:http';
import { ReactionaryFeedServer } from '@reactionary/feeds';

const handler = new ReactionaryFeedServer(createClient, {
  feeds: {
    finnish: finnishFeed,
  },
}).toNodeHandler();

createServer((request, response) => {
  void handler(request, response);
}).listen(3000);

Sitemap sources

Sitemaps use a separate source configuration so product, category, and store URLs can be exposed independently. Use include to choose which configured sources should be listed and served.

new ReactionaryFeedServer(createClient, {
  feeds: {
    finnish: finnishFeed,
  },
  sitemaps: {
    baseUrl: 'https://shop.example',
    include: ['products-fi', 'categories-fi', 'stores-fi'],
    sources: {
      'products-fi': {
        type: 'products',
        feed: 'finnish',
        changefreq: 'daily',
        priority: 0.8,
      },
      'categories-fi': {
        type: 'categories',
        languageContext: {
          locale: 'fi-FI',
          currencyCode: 'EUR',
        },
        urlTemplate: 'https://shop.example/{lang}/categories/{slug}',
        pageSize: 50,
        maxDepth: 10,
      },
      'stores-fi': {
        type: 'stores',
        languageContext: {
          locale: 'fi-FI',
          currencyCode: 'EUR',
        },
        urlTemplate: 'https://shop.example/{lang}/stores/{slug}',
        proximity: {
          longitude: 12.5683,
          latitude: 55.6761,
          distance: 100,
          limit: 100,
        },
      },
    },
  },
});

Product sitemap sources reuse an existing product feed definition. Category sitemap sources enumerate top categories and, by default, recursively include child categories through category.findChildCategories. Store sitemap sources use store.queryByProximity; Reactionary core does not currently expose a provider-neutral “list all stores” operation, so the source requires an explicit proximity query.

Category and store URL templates support {lang}, {slug}, and {id} placeholders.

CLI usage

The CLI loads an ESM config module that default-exports { feeds } or { createClient, feeds }. Sample config files are available in documentation/, including a minimal single-feed config and a multilingual da/nb/sv/fi/en config.

// feeds.config.mjs
export default {
  feeds: {
    finnish: {
      languageContext: {
        locale: 'fi-FI',
        currencyCode: 'EUR',
      },
      search: {
        term: '',
        facets: [],
        filters: [],
        paginationOptions: {
          pageNumber: 1,
          pageSize: 50,
        },
      },
      productUrlBase: 'https://shop.example/{lang}/products/{slug}',
    },
  },
};

Commands:

reactionary-feeds list-feeds --config ./feeds.config.mjs
reactionary-feeds list-transformers --config ./feeds.config.mjs
reactionary-feeds generate \
  --config ./feeds.config.mjs \
  --feed finnish \
  --testMode \
  --output acp-product-feed=./products.jsonl \
  --output acp-feed-metadata=./metadata.json \
  --output google-merchant-feed=./google-merchant.xml \
  --output sitemap-feed=./sitemap.xml

When running directly from this repository before the package is installed, build the package first and call the built CLI:

pnpm exec nx run feeds:build
node dist/packages/feeds/cli.js list-feeds --config ./packages/feeds/documentation/basic-product-feeds.config.mjs

When @reactionary/feeds is installed as a package, npm/pnpm/yarn exposes the reactionary-feeds command from the package bin entry.

Publishing ACP feeds to an agent

ACP product feeds (2026-04-17) are pushed by the merchant to the agent. The agent (e.g. OpenAI) hosts the Feed API — POST /feeds, GET /feeds/{id}, GET/PATCH /feeds/{id}/products — and never calls the merchant for feeds. Your ACP checkout server has no feed endpoints; publishing is a separate job you schedule:

your scheduler (cron, Kubernetes CronJob, CI)
  └─ reactionary-feeds publish-acp ── reads the catalogue (Reactionary client)
                                   └─ PATCH {agent Feed API}/feeds/{id}/products ──▶ agent

From the agent's onboarding you receive the Feed API base URL, an API key and (usually) a feed id per catalogue. Put them in the feeds config — the key stays in the environment:

// feeds.config.mjs
export default {
  acpFeedApi: {
    baseUrl: 'https://feeds.agent.example/api', // from onboarding
    apiKeyEnv: 'ACP_FEED_API_KEY',              // default; the key itself is never in config
  },
  feeds: {
    finnish: {
      languageContext: { locale: 'fi-FI', currencyCode: 'EUR' },
      search: { term: '', facets: [], filters: ['market:fi'], paginationOptions: { pageNumber: 1, pageSize: 50 } },
      productUrlBase: 'https://shop.example/{lang}/products/{slug}',
      acp: { feedId: 'feed_8f3K2x' },          // from onboarding
    },
  },
};

Publish on a schedule. The command prints { "feedId", "products", "batches" } and exits non-zero on any failure, so the scheduler's alerting sees it:

ACP_FEED_API_KEY=... reactionary-feeds publish-acp -c ./feeds.config.mjs -f finnish
# Every 15 minutes
*/15 * * * * ACP_FEED_API_KEY=... reactionary-feeds publish-acp -c /etc/shop/feeds.config.mjs -f finnish --no-progress
apiVersion: batch/v1
kind: CronJob
metadata:
  name: acp-feed-finnish
spec:
  schedule: "*/15 * * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: publish
              image: registry.example/shop-feeds:latest
              args: ["publish-acp", "-c", "/config/feeds.config.mjs", "-f", "finnish", "--no-progress"]
              env:
                - name: ACP_FEED_API_KEY
                  valueFrom: { secretKeyRef: { name: acp-feed-api, key: api-key } }

Options: --batch-size <n> (products per upsert, default 100), --testMode (two search pages only) and --no-progress.

Notes:

  • If the agent does not hand out feed ids, run reactionary-feeds create-acp-feed -c ./feeds.config.mjs -f finnish once; it calls POST /feeds with the feed's target country and prints the id to put into acp.feedId. Nothing is stored at runtime.
  • PATCH upserts by product id and never removes products. Products that should no longer be sold are reported as unavailable when they are still found; to drop products entirely, publish a full replacement.
  • Full replacement instead of the API: reactionary-feeds generate -c ./feeds.config.mjs -f finnish --output acp-product-feed=./products.jsonl --output acp-feed-metadata=./metadata.json, then upload both files however the agent's onboarding specifies (the ACP spec leaves file transfer to the platform).
  • The same publisher is available in code as ReactionaryACPFeedPublisher / createACPFeedPublisherFromConfig (also re-exported from @reactionary/acp).