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

@limova-org/custom-tools

v0.9.0

Published

Limova custom LangChain tool specifications and adapters

Readme

@limova-org/custom-tools

Declarative tool specification catalog for Limova AI agents. This package defines what agents can do without containing any execution logic.

Role in the architecture

custom-tools/                    api/ (NestJS)
  ToolSpecV1 definitions           ToolRegistry
  (schemas, descriptions,    →     PipedreamToolFactory
   Pipedream actions)              ToolExecutionService
  PropsMappers                     PipedreamActionRunner
  LangChain adapters               LLM (Claude, Gemini)

This package is a contract between AI agents and third-party integrations:

  • For the LLM: each spec provides a name, description, and inputSchema (JSON Schema) that the model uses to decide when and how to call a tool
  • For Pipedream: each spec maps to one or more pipedreamActions (e.g. gmail-send-email) executed on the API side
  • For the API: adapters (buildPipedreamTool, buildElevenLabsWebhookTool) convert specs into LangChain DynamicStructuredTool instances

The package has zero runtime dependencies (everything is in peerDependencies). It only exports types, immutable objects, and pure functions.

Run pnpm validate to see the current tool count and domain breakdown.

Key concepts

ToolSpecV1

The fundamental unit. Every tool is an immutable (readonly) object:

interface ToolSpecV1 {
  name: string;                              // e.g. "email_send"
  domain: ToolDomain;                        // e.g. "email"
  description: string;                       // LLM-facing description
  inputSchema: JsonSchemaObjectType;         // tool parameters (JSON Schema)
  providerScoped: boolean;                   // true if multi-provider (Gmail + Outlook)
  providers?: string[];                      // ["gmail", "microsoft_outlook"]
  pipedreamActions?: Record<string, string>; // { gmail: "gmail-send-email" }
}

Domains

Tools are organized by domain. Three categories exist:

  • Pipedream integrations: airtable, brevo, canva, google-sheets, hubspot, notion, shopify, slack, whatsapp-business, etc.
  • Abstract multi-provider domains: email (Gmail/Outlook), calendar (Google Calendar/Outlook)
  • Internal tools: code-interpreter, file-system, retrieval (RAG), shared-memory, date

The full list of domains and tool names is in TOOL_INVENTORY (src/inventory.ts).

PropsMappers

Pure functions that transform LLM arguments (unified schema) into Pipedream-specific props:

LLM: { to: ["[email protected]"], subject: "Hello", body: "..." }
           ↓ sendMapper(args, "gmail")
Pipedream: { to: "[email protected]", subject: "Hello", body: "...", bodyType: "html" }
           ↓ sendMapper(args, "microsoft_outlook")
Pipedream: { recipients: "[email protected]", content: "...", contentType: "html" }

TOOL_INVENTORY

Static manifest (Record<ToolDomain, string[]>) exhaustively listing all tool names per domain. Automatically validated against the registry in CI (pnpm validate) to ensure consistency.

Structure

src/
  index.ts              # Entry point, side-effect imports + re-exports
  registry.ts           # Map<RegistryKey, ToolSpecV1> singleton
  inventory.ts          # TOOL_INVENTORY (static manifest)
  types/
    tool-spec.ts        # ToolSpecV1, JsonSchema*, PhoneAgentWebhookSpec
    adapter.types.ts    # PropsMapper, PipedreamActionRunner, ToolHandler
    registry.types.ts   # RegistryKey, ResolveContext
  adapters/
    langchain.ts        # buildLangchainTool, buildPipedreamTool
    elevenlabs.ts       # buildElevenLabsWebhookTool (phone agent)
  utils/
    json-schema-to-zod.ts  # JSON Schema → Zod conversion for LangChain
  integrations/         # Legacy specs (12 domains: email, calendar, slack...)
  specs/                # Self-registering specs (35+ domains with register.ts)
    <provider>/
      register.ts       # Side-effect registration
      <tool-name>.ts    # Individual spec
      mappers/          # PropsMappers for this provider

Consumption in the API

The package is installed via an npm alias:

"@limova/custom-tools": "npm:@limova-org/[email protected]"

The API imports it as @limova/custom-tools and wraps it behind ToolSpecCatalogService (NestJS facade).

Adding a new integration (end-to-end guide)

This is the full workflow to add tools for a new Pipedream provider (e.g. acme). Use an existing provider like brevo/ or axonaut/ as reference.

Step 1 — Find the Pipedream action IDs

Look up available actions for the provider on the Pipedream registry. Each action has an ID like acme-create-contact. You'll need these IDs for the pipedreamActions field.

You can also fetch them programmatically:

# List all available actions for a component
curl -s "https://api.github.com/repos/PipedreamHQ/pipedream/contents/components/acme/actions"

# Get the detailed schema of a specific action (props, types, required fields)
curl -s \
  -H "Authorization: Bearer <pipedream-registry-token>" \
  "https://api.pipedream.com/v1/components/registry/acme-create-contact"

The registry response contains configurable_props with prop names, types, descriptions, and required/optional status — use this as the source of truth for your inputSchema.

Step 2 — Create the spec files

Create a folder src/specs/acme/ with one file per tool:

// src/specs/acme/create-contact.ts
import type { ToolSpecV1 } from '@/types/tool-spec';

export const acmeCreateContactSpec: ToolSpecV1 = {
  name: 'acme_create_contact',
  domain: 'acme',
  description: 'Create a new contact in Acme CRM.',
  providerScoped: false,
  inputSchema: {
    type: 'object',
    properties: {
      email: {
        type: 'string',
        description: 'Email address of the contact.',
      },
      name: {
        type: 'string',
        description: 'Full name of the contact.',
      },
    },
    required: ['email'] as const,
  },
  tags: ['acme', 'crm', 'contact', 'create'],
  pipedreamActions: {
    acme: 'acme-create-contact',
  },
};

Key rules:

  • name follows the pattern <domain>_<action> (snake_case)
  • description is LLM-facing — be clear and concise about what the tool does
  • Every property in inputSchema must have a description
  • pipedreamActions maps the provider slug to the Pipedream action ID

Step 3 — Create the register file

// src/specs/acme/register.ts
import { registerToolSpec } from '@/registry';

import { acmeCreateContactSpec } from './create-contact';

const ACME_SPECS = [
  acmeCreateContactSpec,
] as const;

ACME_SPECS.forEach(registerToolSpec);

Step 4 — Wire the side-effect import

In src/index.ts, add the import at the top with the other register imports (alphabetical order):

import './specs/acme/register';

Then add re-exports for the spec constants at the bottom:

export { acmeCreateContactSpec } from './specs/acme/create-contact';

Step 5 — Add to TOOL_INVENTORY

In src/inventory.ts, add the new domain entry (alphabetical order):

acme: [
  'acme_create_contact',
],

Step 6 — Add a PropsMapper (if needed)

If the Pipedream action expects props in a different format than the inputSchema, create a mapper:

// src/specs/acme/mappers/create-contact.mapper.ts
import type { PropsMapper } from '@/types/adapter.types';

export const acmeCreateContactMapper: PropsMapper = (args, _provider) => {
  return {
    ...args,
    // transform args to match Pipedream's expected props
  };
};

Export it from src/index.ts:

export { acmeCreateContactMapper } from './specs/acme/mappers/create-contact.mapper';

Step 7 — Validate and test

pnpm build && pnpm validate && pnpm test

pnpm validate checks that every name in TOOL_INVENTORY has a matching spec in the registry, and vice versa.

Step 8 — Verify against Pipedream docs

Before publishing, verify your implementation against the official Pipedream registry. For each tool, fetch its schema and compare:

# Verify action ID exists and get its props
curl -s \
  -H "Authorization: Bearer <pipedream-registry-token>" \
  "https://api.pipedream.com/v1/components/registry/acme-create-contact"

Check that:

  • Action IDs in pipedreamActions exist in the registry
  • Prop names and types match configurable_props
  • Required vs optional fields are correct

Fix any mismatches before publishing.

Step 9 — Publish

git add . && git commit -m "feat: add acme integration"
git push origin main

The CI will automatically publish a new version to npm.

Step 10 — Install in the API

After the npm release, update the dependency in the API:

cd ../api
pnpm update @limova-org/custom-tools @limova/custom-tools

If the tools need a PropsMapper, wire it in api/src/modules/agentic/tools/pipedream/constants/pipedream-props-mappers.constants.ts:

import { acmeCreateContactMapper } from '@limova/custom-tools';

export const PIPEDREAM_PROPS_MAPPERS: PropsMappersByToolName = {
  acme_create_contact: acmeCreateContactMapper,
  // ...existing mappers
};

If the tools need dynamic props resolution (Pipedream reloadProps), add the spec names to SPECS_REQUIRING_DYNAMIC_PROPS in api/src/modules/agentic/tools/pipedream/constants/pipedream-tool-factory.constants.ts.

Step 11 — Test end-to-end

Test the integration by sending a message through the Limova chat that triggers the new tools. Verify that the tool calls execute successfully and return the expected results.

Step 12 — Assign tools to an agent

In the database, add the tool names to the agent's tools JSONB array. The tools will be loaded automatically on the next agent reload (POST /agent/reload).

Common pitfalls

PropsMapper must have 2+ parameters. The API uses isPropsMapper() which checks function.length >= 2. A mapper with only (args) will be silently ignored. Always use (args, _provider).

{ type: 'object' } without properties. If an inputSchema property is { type: 'object' } with no properties field (e.g. a dynamic fields or data object), jsonSchemaToZod converts it to z.record(z.string(), z.unknown()). If you accidentally add an empty properties: {}, it becomes z.object({}) which strips all dynamic keys silently at runtime.

Dynamic props (reloadProps). Some Pipedream actions use additionalProps() / reloadProps: true to resolve their schema dynamically (e.g. Airtable fields, Brevo lists). These specs must be added to SPECS_REQUIRING_DYNAMIC_PROPS in the API, otherwise the props won't be resolved and the action will fail.

Provider slug mismatches. Some providers have special slugs in Pipedream. For example, Slack uses slack_v2 as the app slug but action IDs use the slack-* prefix (not slack_v2-*). Check APP_PROP_NAME_OVERRIDES in the API if auth fails with not_authed.

Local development tip: use pnpm local-libs:refresh in api/ to test changes without publishing to npm. Use pnpm start:dev:local-libs for hot-reload across both packages.

Setup

pnpm install

Scripts

pnpm build       # Build with tsup (ESM + CJS + types)
pnpm lint        # ESLint fix
pnpm typecheck   # TypeScript check
pnpm test        # Run tests (vitest)
pnpm validate    # Validate TOOL_INVENTORY vs registry

Release and npm publishing

How it works

The project uses release-it with the @release-it/conventional-changelog plugin to automate releases.

On every push to main, the CI:

  1. Runs build, validate, lint, typecheck, tests
  2. Analyzes commits since the last tag
  3. Determines the version bump based on conventional commits
  4. Updates package.json and CHANGELOG.md
  5. Creates a release commit, a git tag, and a GitHub Release
  6. Publishes to npm

If there are no triggering commits (fix: or feat:), the release step does nothing.

Conventional commits

| Prefix | Release | Bump | Example | |--------|---------|------|---------| | fix: | Yes | patch (0.0.x) | fix: correct schema validation | | feat: | Yes | minor (0.x.0) | feat: add slack integration | | feat!: | Yes | major (x.0.0) | feat!: rename tool spec format | | chore: | No | - | chore: update dependencies | | docs: | No | - | docs: update README | | refactor: | No | - | refactor: simplify adapter logic | | test: | No | - | test: add missing unit tests |

A BREAKING CHANGE: in the commit body also triggers a major bump.

Manual release (optional)

To trigger a release locally (interactive mode):

pnpm release

Configuration

  • .release-it.json — release-it configuration
  • .github/workflows/release.yml — CI workflow

npm configuration (one-time setup)

Billing prerequisites

To publish private packages under the @limova-org scope, you need:

  1. A paid plan on the limova-org npm organization (npm Teams)
  2. A paid plan on the publisher's personal npm account

npm requires both plans for private scoped packages. Without the personal paid plan, publish fails with E402 Payment Required - You must sign up for private packages, even if the organization is on a paid plan.

npm token

  1. Go to npmjs.com > avatar > Access Tokens > Generate New Token
  2. Choose Granular Access Token
  3. Configure:
    • Organizations: select limova-org
    • Packages: "All packages" or specific packages, permissions Read and write
    • Bypass two-factor authentication (2FA): check (required for CI)
  4. Copy the token

GitHub Actions secret

  1. In the GitHub repo > Settings > Secrets and variables > Actions > New repository secret
  2. Name: NPM_TOKEN, value: the Granular Access Token

First publish

The first publish of a new package must be done manually locally:

npm login
npm publish

Subsequent publications are handled automatically by the CI.

Important: the @limova-org scope matches the exact npm organization name. If the org name changes, update the name field in package.json.