npm package: @limova-org/custom-tools
Description: Limova custom LangChain tool specifications and adapters
Latest version: 0.9.0

# @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:

```typescript
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: ["a@b.com"], subject: "Hello", body: "..." }
           ↓ sendMapper(args, "gmail")
Pipedream: { to: "a@b.com", subject: "Hello", body: "...", bodyType: "html" }
           ↓ sendMapper(args, "microsoft_outlook")
Pipedream: { recipients: "a@b.com", 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:

```json
"@limova/custom-tools": "npm:@limova-org/custom-tools@0.7.9"
```

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](https://pipedream.com/apps). Each action has an ID like `acme-create-contact`. You'll need these IDs for the `pipedreamActions` field.

You can also fetch them programmatically:

```bash
# 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:

```typescript
// 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

```typescript
// 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):

```typescript
import './specs/acme/register';
```

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

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

#### Step 5 — Add to TOOL_INVENTORY

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

```typescript
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:

```typescript
// 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`:

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

#### Step 7 — Validate and test

```bash
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:

```bash
# 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

```bash
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:

```bash
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`:

```typescript
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

```bash
pnpm install
```

## Scripts

```bash
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](https://github.com/release-it/release-it) with the [@release-it/conventional-changelog](https://github.com/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](https://www.conventionalcommits.org/)
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):

```bash
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](https://www.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:

```bash
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`.