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

@lunnoa/client

v1.2.0

Published

TypeScript SDK for building applications on top of Lunnoa Automate — typed client for the Public API (agents, workflows, executions, entities, knowledge, queues), SSE agent chat streaming, and deployment-specific codegen

Downloads

30

Readme

@lunnoa/client

TypeScript SDK for building applications on top of Lunnoa Automate.

npm version license

What is this?

@lunnoa/client is the official consumption SDK for the Lunnoa Automate Public API: a typed client for agents and chat (with SSE streaming), workflows and executions, entities (Objects), knowledge, queues, variables, connections, and projects — plus a codegen command that types the SDK against your deployment's entity types, workflows, and agents.

The boundary with the other Lunnoa SDK: @lunnoa/toolkit is for building integration apps that run inside the platform (createAction / createApp); @lunnoa/client is for building applications on top of the platform — including define* factories, runs.start, and CLI deploy of workflows/agents.

The SDK is deliberately presentation-free at the core: no components or widgets. Optional headless React hooks ship as @lunnoa/client/react. You build fully custom UIs in your own design system; the SDK encodes the platform knowledge (protocols, schemas, conventions). Agent skills for Cursor / Claude Code ship in skills/ (see AI-assisted building).

Installation

npm install @lunnoa/client

Requirements: Node.js ≥ 20 (or any runtime with fetch and web streams).

Quickstart

import { LunnoaClient } from '@lunnoa/client';

const lunnoa = new LunnoaClient({
  baseUrl: 'https://lunnoa.your-company.example',
  apiKey: process.env.LUNNOA_API_KEY, // lna_... — server-side only
});

const { data: invoices } = await lunnoa.entities.list({
  objectTypeSlug: 'invoice',
  state: 'pending',
  expansion: ['attributes'],
});

A SuperAdmin creates API keys in Adminspace → API keys, choosing the workspace and an RBAC role for the key's service account. The role is the key's entire capability model — grant the minimum permission set, not Admin.

Two auth patterns

| Pattern | Credential | Where | |---|---|---| | A. Backend holds the key | apiKey: 'lna_...' via createServerClient | Your server, background jobs, server-rendered pages. The key acts as a workspace-scoped service account. | | B. End users are Lunnoa users | User JWT via createBrowserClient + lunnoa.auth.* | Browser apps. Login, 2FA, refresh, and SSO discovery are wrapped; per-user permissions apply. |

import {
  createBrowserClient,
  createServerClient,
  isLoginRequires2FA,
} from '@lunnoa/client';

// Pattern A — server-side
const server = createServerClient({
  baseUrl,
  apiKey: process.env.LUNNOA_API_KEY!,
});

// Pattern B — browser (tokens in localStorage by default; auto-refresh on 401)
const lunnoa = createBrowserClient({ baseUrl });
const result = await lunnoa.auth.login({ email, password });
if (isLoginRequires2FA(result)) {
  await lunnoa.auth.verify2faLogin({
    sessionToken: result.sessionToken,
    token: totpCode,
  });
}
const me = await lunnoa.auth.me();

API keys never ship to browsers. The SDK enforces this: constructing a client with an lna_ key while window exists throws immediately. Browser apps either use Pattern B or call your own backend, which holds the key (backend-for-frontend).

For Pattern B the deployment must allow your origin via its CORS_ALLOWED_ORIGINS configuration. See the Authentication guide under guides/develop/client/auth in Lunnoa Docs for SSO redirect constraints and token stores.

React (optional)

import {
  LunnoaAuthProvider,
  useLunnoaAuth,
  useLunnoaClient,
  useExecutionProgress,
  useNeedsInput,
  useAgentChat,
  useApprovalsInbox,
  useEntityList,
} from '@lunnoa/client/react';

<LunnoaAuthProvider baseUrl={baseUrl}>
  <App />
</LunnoaAuthProvider>

const { user, status, login, logout } = useLunnoaAuth();
const lunnoa = useLunnoaClient();
const { progress, status: progressStatus } = useExecutionProgress(executionId);
const { waiting, pendingInput, submitInput } = useNeedsInput(progress);
const { messages, send, stop, taskId } = useAgentChat(agentId); // needs @ai-sdk/react
const { items, decide } = useApprovalsInbox({ pollIntervalMs: 15_000 });
const { data, page, setPage } = useEntityList({ objectTypeSlug: 'invoice' });

Peers: react ≥ 18 (required for /react); @ai-sdk/react only for useAgentChat. Core @lunnoa/client stays headless.

Resources

Most namespaces map to the Public API surface (openapi.public.json):

auth · agents · tasks · agentChat (SSE streaming) · workflows · executions · actions · entities · entityTypes · knowledge · queues · queueItems · variables · connections · projects · workflowApps · discovery

approvals uses the authenticated workspace API (/api/approvals/...), not Public API v1. The inbox is filtered to requests where the current user is an eligible approver.

Errors are LunnoaApiError with status, body, and convenience flags (isUnauthorized, isForbidden, isRateLimited). Paginated lists have async iteration (entities.iterate(...), queueItems.iterate(...)).

Worked examples

Runnable versions live in examples/.

Query and create entities

const invoiceType = await lunnoa.entityTypes.getBySlug('invoice', {
  expansion: ['attributeSchema', 'stateSchema'],
});

const created = await lunnoa.entities.create({
  name: 'INV-2026-0042',
  objectTypeId: invoiceType.id,
  attributes: { amount: 1250.5, currency: 'CHF' }, // keyed by field ID
});

for await (const invoice of lunnoa.entities.iterate({ objectTypeSlug: 'invoice' })) {
  // pages fetched lazily
}

Trigger a workflow and wait for the result

const { id } = await lunnoa.workflows.execute(workflowId, { customerEmail: '[email protected]' });
const execution = await lunnoa.executions.waitUntilFinished(id); // poll with backoff
console.log(execution.status, execution.output);

// or in one call:
const done = await lunnoa.workflows.executeAndWait(workflowId, inputs);

Run a single app action (ad-hoc Execution)

Creates a one-step Execution with source: SDK (same inbox as workflow runs). Discover appId / actionId / inputConfig via workflowApps.list(). Pass connectionId (the Connection instance UUID from Connections) when the action needs a connection and more than one exists for the app.

const result = await lunnoa.actions.run({
  appId: 'http',
  actionId: 'http_action_send-request',
  // connectionId: '...', // Connection.id UUID when needed
  input: { method: 'GET', url: 'https://example.com' },
});
console.log(result.status, result.output, result.executionPath);

// Same record via the Executions API:
const again = await lunnoa.executions.get(result.id, {
  expansion: ['status', 'source', 'executionPath'],
});

Resume a NEEDS_INPUT execution from a custom form

Workflows can pause and wait for human input. Render a form for the waiting step and resume:

const execution = await lunnoa.executions.get(executionId, {
  expansion: ['status', 'executionPath'],
});
const waiting = execution.executionPath?.find((s) => s.status === 'NEEDS_INPUT');

await lunnoa.executions.submitInput(executionId, waiting!.nodeId, {
  approved: true,
  comment: 'Release the payment.',
});
const finished = await lunnoa.executions.waitUntilFinished(executionId);

The executionPath expansion is the business-readable step list (labels, per-step status, timings, loop iterations) custom UIs render progress from.

Approvals inbox (Request Approval nodes)

List pending approvals the authenticated user can decide, then approve or reject. Prefer a user JWT (pattern B) so eligibility matches a real person. Requires approvals:read / approvals:decide.

const { items } = await lunnoa.approvals.inbox(); // pending + partially_approved

for (const person of items[0]?.eligibleApprovers ?? []) {
  console.log(person.email, person.name); // who can still decide
}

await lunnoa.approvals.decide(items[0]!.approvalId, {
  decision: 'approved',
  comment: 'Within policy limits.',
});

// Or by execution + node when you already have those IDs:
await lunnoa.executions.submitApproval(executionId, nodeId, {
  decision: 'rejected',
  comment: 'Amount exceeds limit.',
});

Do not use submitInput for Request Approval nodes; that path is for generic NEEDS_INPUT forms.

Stream agent chat

The streaming module is hand-written (code generators handle SSE poorly) and speaks the same wire protocol as the bundled UI: AI SDK UIMessage chunks, SSE-framed, resumable.

const taskId = crypto.randomUUID(); // first message creates the task
const stream = await lunnoa.agentChat.streamMessage(agentId, taskId, 'Hello!');

for await (const chunk of stream) {
  if (chunk.type === 'text-delta') process.stdout.write(chunk.delta);
}

// Reattach after a disconnect (returns null when nothing is running):
const resumed = await lunnoa.agentChat.resumeStream(agentId, taskId);

// Cancel a running turn (optionally persisting the partial reply):
await lunnoa.agentChat.stop(agentId, taskId);

stream.toUIMessageStream() returns a ReadableStream<UIMessageChunk> ready for the AI SDK's readUIMessageStream when you want assembled UIMessage snapshots for rendering.

Deployment-specific codegen

Entity types, workflow inputs, and agents are defined per deployment, so the generic SDK can only type them loosely. The codegen command queries your deployment and emits typed accessors — the same developer experience as Prisma client or Supabase type generation:

npx @lunnoa/client codegen --url https://lunnoa.your-company.example --api-key lna_... --out ./src/lunnoa

This writes lunnoa.generated.ts:

import { LunnoaClient } from '@lunnoa/client';
import { createDeploymentClient } from './lunnoa/lunnoa.generated';

const lunnoa = createDeploymentClient(new LunnoaClient({ baseUrl, apiKey }));

// Entity attributes and states are compile-time checked:
const open = await lunnoa.entities.invoice.list({ state: 'pending' });
await lunnoa.entities.invoice.create({
  name: 'INV-1',
  attributes: { amount: 99.5, status: 'pending' },
});

// Workflow inputs typed from the manual trigger's input schema:
await lunnoa.workflows.processInvoice.executeAndWait({ invoiceId: 'abc' });

// Agents by name:
const stream = await lunnoa.agents.supportAgent.streamMessage(taskId, 'Hi');

Re-run codegen whenever entity types, workflows, or agents change on the deployment.

Define, run, and CLI deploy

Author local definitions with defineWorkflow / defineAgent / defineEntityType, run linear chains with lunnoa.runs.start, and upsert with the CLI (prefer CLI over calling upsert from app code). Entity types are workspace-scoped (no --project-id); instance writes reject unknown attribute keys unless allowUnknownAttributes is true — use the reserved extensions object for flexible bags:

npx @lunnoa/client entity-types deploy ./human.ts --url https://lunnoa.example --api-key lna_...

Workflows and agents:

npx @lunnoa/client workflows deploy ./workflows/sync-orders.ts \
  --url https://lunnoa.example --api-key lna_... --project-id <uuid>
npx @lunnoa/client agents deploy ./agents/triage.ts \
  --url https://lunnoa.example --api-key lna_... --project-id <uuid>

defineAction binds catalogue actions only; toolkit createAction is for in-platform apps. See Lunnoa Docs under guides/develop/client/define, runs, and cli-deploy.

CLI discovery

Coding agents (and humans) should list the catalogue for a given key instead of guessing action ids. --json is the machine-readable form.

npx @lunnoa/client tools list --url … --api-key lna_… --for-agent --json
npx @lunnoa/client tools search email --for-agent --json
npx @lunnoa/client tools get http_action_send-request --json
npx @lunnoa/client apps list --json
npx @lunnoa/client apps get http --json
npx @lunnoa/client connections list --app gmail --json
npx @lunnoa/client agents list --json
npx @lunnoa/client workflows list --json
npx @lunnoa/client entity-types list --json
npx @lunnoa/client projects list --json
npx @lunnoa/client features --json

tools list is compact (appId, actionId, name, flags). tools get prints inputConfig and a defineAction({ id, appId, input }) sketch. Auth flags and LUNNOA_URL / LUNNOA_API_KEY / lunnoa.config work the same as deploy.

Regenerating the API types

The generic types are generated from the deployment's public OpenAPI spec (spec/openapi.public.json, produced by the platform's export-openapi --public):

npm run codegen:spec

AI-assisted building

Two Cursor / Claude Code skills ship in the published package under skills/:

| Skill | Path | Role | |---|---|---| | lunnoa-solution-design | skills/lunnoa-solution-design/SKILL.md | Interview the human, map intent to Lunnoa primitives, produce a build brief | | lunnoa-client | skills/lunnoa-client/SKILL.md | Implement with auth, discovery, codegen, define* / runs / CLI deploy, React hooks |

After install, copy or enable them from node_modules/@lunnoa/client/skills/ into your app’s .cursor/skills/ (or Claude Code skills directory). Prefer solution-design first when requirements are unclear, then lunnoa-client.

Partner paste block:

Use @lunnoa/client skills: lunnoa-solution-design then lunnoa-client.
LUNNOA_URL=… LUNNOA_API_KEY=lna_… (server only). Project ID=…
Start by interviewing me, then workflowApps.list() / codegen.

Docs quickstart: For agents (or your deployment’s docs site).

Versioning and stability

This package follows semantic versioning. It targets the Lunnoa Automate Public API v1: endpoints marked stable by the platform's x-public contract. Public endpoints only change additively; breaking platform changes come with a deprecation window.

License

Licensed under Apache-2.0 with Commons Clause.

You may use, modify, and distribute the software under Apache 2.0 terms, with one additional restriction: you may not sell the software itself (the SDK as a standalone product). Building and operating applications that use the SDK is permitted.

Development

git clone https://github.com/lunnoa-automate/lunnoa_client.git
cd lunnoa_client
pnpm install
pnpm test
pnpm build

Build output (ESM + CJS + type declarations) is written to dist/. The published npm tarball includes dist/ and skills/.

Publishing

Releases are fully automated with semantic-release, the same setup as @lunnoa/toolkit: merge Conventional Commits to master/main, CI runs tests and build, and the Release workflow computes the version, updates CHANGELOG.md, tags, creates the GitHub release, and publishes to npm via trusted publishing (OIDC).

| Commit prefix | Version bump | | --- | --- | | fix: | Patch | | feat: | Minor | | feat!: / BREAKING CHANGE: | Major |

npm trusted publishing (OIDC)

Publishing uses npm trusted publishers via GitHub Actions OIDC — no long-lived NPM_TOKEN is required after the package exists.

Chicken-and-egg for the first publish: npm only lets you attach a Trusted Publisher to a package that already exists. Bootstrap once (token or placeholder publish), then configure OIDC and prefer that for all later releases.

  1. Publish the initial version (e.g. via the Bootstrap lunnoa_client publish workflow in lunnoa_automate, or NPM_TOKEN on the Release workflow).
  2. On npmjs.com (org admin for @lunnoa), open the package Access tab and add a Trusted Publisher:

| Field | Value | | --- | --- | | Package | @lunnoa/client | | Organization / user | lunnoa-automate | | Repository | lunnoa_client | | Workflow filename | release.yml | | Environment | (leave empty) |

  1. After OIDC works, you can remove NPM_TOKEN from the Release workflow env.

The release workflow grants id-token: write and uses semantic-release v25 with Node 24.

Acceptance soak

Against a real deployment (service-account API key with at least entities:read, entities:create, agents:use):

LUNNOA_URL=https://your-deployment \
LUNNOA_API_KEY=lna_... \
AGENT_ID=<agent-uuid> \
ENTITY_TYPE_SLUG=<optional-slug> \
pnpm exec tsx scripts/acceptance-soak.ts