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

@molecule/api-ai-decisions-molecule

v1.1.1

Published

molecule.dev hosted AI decisions — typed choice/score/yes-no answers billed to your molecule project, no vendor account

Readme

@molecule/api-ai-decisions-molecule

molecule.dev hosted AI decisions — typed choice/score/yes-no answers with probabilities, billed to your molecule project

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Type

provider

Installation

npm install @molecule/api-ai-decisions-molecule @molecule/api-ai-decisions @molecule/api-secrets

API

Interfaces

MoleculeDecisionsConfig

Options for {@link createProvider}. Every field falls back to an env var, so the zero-argument provider export works from .env alone.

interface MoleculeDecisionsConfig {
  /**
   * The molecule project API key (`mk_…`), or the sandbox token molecule.dev
   * writes for in-IDE apps (`mbk_…`). Defaults to `MOLECULE_API_KEY`.
   */
  apiKey?: string
  /**
   * The hosted services base URL, without a trailing slash. Defaults to
   * `MOLECULE_SERVICES_URL`, then `https://api.molecule.dev/api/v1/services`.
   */
  servicesUrl?: string
  /** Per-request timeout in milliseconds. Default 30000 — deciding is a model round trip. */
  timeoutMs?: number
}

ProcessEnv

Environment variables this provider reads.

interface ProcessEnv {
  MOLECULE_API_KEY?: string
  MOLECULE_SERVICES_URL?: string
}

Classes

MoleculeServiceError

Error thrown for a refused or failed hosted-service call.

class MoleculeServiceError extends Error {
  /** HTTP status from molecule.dev. */
  readonly status: number
  /** Stable error key from molecule.dev. */
  readonly errorKey: string | undefined

  /**
   * Create the error.
   *
   * @param message - Human-readable message.
   * @param status - HTTP status.
   * @param errorKey - Stable error key, when the service sent one.
   * @param cause - The underlying error, if any.
   */
  constructor(message: string, status: number, errorKey?: string, cause?: unknown) {
    super(message, cause === undefined ? undefined : { cause })
    this.name = 'MoleculeServiceError'
    this.status = status
    this.errorKey = errorKey
  }
}

MoleculeDecisionsProvider

AI decisions provider backed by molecule.dev's hosted service.

The model is the SERVICE's choice — DecideInput.model (a self-hosted bond's checkpoint selector) is not forwarded, so a call that sets it keeps running on the hosted default instead of failing.

class MoleculeDecisionsProvider implements AIDecisionsProvider {
  readonly name = 'molecule'

  private readonly apiKey: string
  private readonly servicesUrl: string
  private readonly timeoutMs: number

  /**
   * Create the provider.
   *
   * @param config - Options; each falls back to its env var.
   */
  constructor(config: MoleculeDecisionsConfig = {}) {
    this.apiKey = config.apiKey ?? process.env.MOLECULE_API_KEY ?? ''
    this.servicesUrl = resolveServicesUrl(
      config.servicesUrl ?? process.env.MOLECULE_SERVICES_URL ?? DEFAULT_SERVICES_URL,
    )
    this.timeoutMs = config.timeoutMs ?? 30_000
  }

  /**
   * Decide: answer the questions about the state.
   *
   * @param input - The state, questions, optional images and confidence floor.
   * @returns Typed answers keyed like the questions, with probabilities.
   */
  async decide<Q extends Record<string, DecisionQuestion> = Record<string, DecisionQuestion>>(
    input: DecideInput<Q>,
  ): Promise<DecideResult<Q>> {
    const questionCount = Object.keys(input.questions ?? {}).length
    if (questionCount === 0) {
      throw new MoleculeServiceError(
        '"questions" must be a non-empty object keyed by question id.',
        400,
        'hostedServices.error.invalidInput',
      )
    }
    if (questionCount > DECISIONS_SERVICE_LIMITS.maxQuestions) {
      throw new MoleculeServiceError(
        `${questionCount} questions is over the limit of ${DECISIONS_SERVICE_LIMITS.maxQuestions}. ${HINTS[413]}`,
        413,
        'hostedServices.error.inputTooLarge',
      )
    }
    if (
      typeof input.state === 'string' &&
      input.state.length > DECISIONS_SERVICE_LIMITS.maxStateChars
    ) {
      throw new MoleculeServiceError(
        `The state is ${input.state.length} characters; the limit is ${DECISIONS_SERVICE_LIMITS.maxStateChars}.`,
        413,
        'hostedServices.error.inputTooLarge',
      )
    }
    const body: Record<string, unknown> = {
      state: input.state,
      questions: input.questions,
      ...(input.images ? { images: input.images } : {}),
      ...(input.minConfidence !== undefined ? { minConfidence: input.minConfidence } : {}),
    }
    const signal = input.signal
      ? AbortSignal.any([input.signal, AbortSignal.timeout(this.timeoutMs)])
      : AbortSignal.timeout(this.timeoutMs)
    return this.request(body, signal) as Promise<DecideResult<Q>>
  }

  /** One POST to the hosted service. */
  private async request(
    body: Record<string, unknown>,
    signal: AbortSignal,
  ): Promise<DecideResult<Record<string, DecisionQuestion>>> {
    if (!this.apiKey) {
      throw new MoleculeServiceError(
        'MOLECULE_API_KEY is not set. Create a project API key on molecule.dev (or with `mlcl apikey create`) and put it in the API .env.',
        401,
        'hostedServices.error.tokenInvalid',
      )
    }
    const response = await fetch(`${this.servicesUrl}/ai-decisions/decide`, {
      method: 'POST',
      headers: { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' },
      body: JSON.stringify(body),
      signal,
    })
    const text = await response.text()
    let data: unknown
    try {
      data = text ? JSON.parse(text) : {}
    } catch (error) {
      // A proxy/gateway error page, not the service: report the status only.
      throw new MoleculeServiceError(
        `molecule.dev returned ${response.status} with a non-JSON body.`,
        response.status,
        undefined,
        error,
      )
    }
    if (!response.ok) {
      const err = data as { error?: string; errorKey?: string }
      const hint = HINTS[response.status]
      throw new MoleculeServiceError(
        `molecule.dev ai-decisions/decide failed (${response.status}): ${err.error ?? 'error'}${hint ? ` ${hint}` : ''}`,
        response.status,
        err.errorKey,
      )
    }
    return data as DecideResult<Record<string, DecisionQuestion>>
  }
}

Functions

createProvider(config)

Create a hosted AI decisions provider.

function createProvider(config?: MoleculeDecisionsConfig): AIDecisionsProvider
  • config — Options; each falls back to its env var.

Returns: An AIDecisionsProvider backed by molecule.dev.

Constants

provider

The provider implementation (wire with setProvider).

const provider: AIDecisionsProvider

DEFAULT_SERVICES_URL

Default hosted services base URL.

const DEFAULT_SERVICES_URL: 'https://api.molecule.dev/api/v1/services'

DECISIONS_SERVICE_LIMITS

Per-request limits of the hosted service (mirrored for local refusal).

const DECISIONS_SERVICE_LIMITS: {
  readonly maxStateChars: 100000
  readonly maxStateJsonBytes: 200000
  readonly maxQuestions: 10
  readonly maxImages: 3
  readonly maxImageBytes: number
}

aiDecisionsMoleculeSecretDefinitions

Secret definitions required by the molecule.dev hosted AI decisions bond.

const aiDecisionsMoleculeSecretDefinitions: SecretDefinition[]

Core Interface

Implements @molecule/api-ai-decisions interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-molecule'

export function setupAiDecisionsMolecule(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-ai-decisions >=1.1.0
  • @molecule/api-secrets ^1.0.1

Environment Variables

  • MOLECULE_API_KEY (required) — molecule.dev project API key
    • Setup: Create one for your project with mlcl apikey create --project <id> --name <name> (keys start with mk_).
    • Get it here: https://www.molecule.dev
    • Example: mk_...

Runtime Dependencies

  • @molecule/api-ai-decisions
  • @molecule/api-secrets

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • [ ] Each flow that makes a decision (routing, triage, moderation, a guardrail) runs it from the real UI, and the answer DRIVES what happens next (the item lands in the chosen queue, the badge shows, the action is blocked) — not just printed.
  • [ ] Both directions: a clearly-billing input routes to billing AND a clearly-technical one routes elsewhere. One label for every input is a broken integration.
  • [ ] A low-confidence answer takes the app's fallback path (human review, "unsure" state) instead of being acted on.
  • [ ] Provider errors (service down, bad key) show a visible, recoverable state — never a blank screen or an unhandled rejection.
  • [ ] The call runs server-side: no provider request or key in the browser's Network tab.