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

@pliuz/sdk

v0.5.0

Published

Human-in-the-loop approval gates for AI agents — pause function calls for human review, then resume or abort.

Readme

@pliuz/sdk — TypeScript SDK

npm Node License

Human-in-the-loop approval gates for AI agents. Wrap any function in gated() to pause execution, route the call to a human approver via Pliuz, then resume — or abort — based on the decision. Every call is audited.

import { gated } from '@pliuz/sdk'

const issueRefund = gated(
  {
    policy: 'refund',
    redact: ['customer.ssn'],
    toolArgs: (customerId: string, amountCents: number) => ({
      customer_id: customerId,
      amount_cents: amountCents,
    }),
  },
  async (customerId: string, amountCents: number) => {
    return stripe.refunds.create({ customer: customerId, amount: amountCents })
  },
)

// Pauses. A human gets a Slack card. They click approve.
// Then your code runs — and Pliuz records the outcome.
const result = await issueRefund('cus_123', 5000)

Install

npm install @pliuz/sdk
# or, with a framework adapter
npm install @pliuz/sdk ai                              # Vercel AI SDK
npm install @pliuz/sdk @anthropic-ai/claude-agent-sdk  # Claude Agent SDK
npm install @pliuz/sdk @openai/agents                  # OpenAI Agents SDK

Requires Node ≥ 18.17 (uses native fetch + AbortController).

Quickstart

1. Get an API key

Sign up at pliuz.com, create an agent in the dashboard, copy the key.

2. Set the env var

export PLIUZ_API_KEY=pli_live_...

3. Gate a function

import { gated } from '@pliuz/sdk'

const runSql = gated(
  { policy: 'risky_query' },
  async (query: string) => db.execute(query),
)

// Blocks until a human approves (or rejects, or it expires).
const rows = await runSql("DELETE FROM users WHERE created_at < '2024-01-01'")

That's it. Pliuz handles the rest: idempotency, retries, audit logging, hash-chained event log.

What gated() does

yourFn(...args)                                  yourFn(...args)
     │                                                  │
     ▼                                                  ▼
 ┌──────────┐  POST /approvals                    ┌──────────┐
 │  gated   │ ───────────────►  ┌─────────────┐  │  gated   │
 │ wrapper  │                   │   Pliuz API  │  │ wrapper  │
 │          │  ◄─────────────── └─────────────┘  │          │
 └──────────┘     status=pending          ▲      └──────────┘
     │                                    │           │
     │              poll every 2s         │           ▼
     │ ────────────────────────────────► humans  ┌──────────┐
     ▼                                    │      │ original │
 ┌──────────┐  status=approved            │      │   fn()   │
 │ execute  │ ◄──────────────────────────┘      └──────────┘
 │ original │                                         │
 │   fn()   │  POST /:id/execution                    ▼
 └──────────┘ ─────────────────────────►       audit log
     │
     ▼
   result

Features

  • Zero dependencies — uses native fetch + AbortController
  • Dual ESM/CJS — works in any modern Node project
  • First-class TypeScript — fully typed, including narrow error subclasses
  • Idempotency via deterministic key hashing — safe to retry from anywhere
  • Client-side redaction — sensitive fields never leave your process plaintext (applied to tool args AND the execution excerpt)
  • Bounded waiting via timeoutMs (first-class AbortSignal cancellation is on the roadmap — not shipped yet)
  • Human edits honored — if an approver edits the args, the edited args execute (or a typed error is thrown), never the original ones
  • Single-shot execution reporting — awaited before your call returns, closing the audit loop reliably on serverless
  • Typed errorsPliuzRejectedError, PliuzApprovalExpiredError, PliuzPolicyError, etc.
  • Framework adaptersgatedTool() wraps the framework's tool so existing agents gate transparently. Dedicated adapters for the Vercel AI SDK (/adapters/ai), the Claude Agent SDK (/adapters/claude-agent), and the OpenAI Agents SDK (/adapters/openai-agents) — any other framework still works via gated() on your own functions

API

gated() — the headline

import { gated } from '@pliuz/sdk'

const wrapped = gated(
  {
    policy: 'refund',                  // Pliuz policy slug (optional)
    redact: ['customer.ssn'],          // Dotted paths to strip BEFORE sending
    timeoutMs: 300_000,                // Max polling duration (5 min default)
    pollIntervalMs: 2000,              // Time between status checks
    toolName: 'custom_name',           // Override fn.name
    toolArgs: (...args) => ({ ... }),  // Map positional args → tool_args object
    client: pliuz,                     // Reuse a client (default: lazy from env)
    contextMessages: ['extra info for the human'],
    sessionId: 'trace-abc',
    originator: { type: 'user', id: 'user-42' },
  },
  async (...args) => { ... },
)

Why toolArgs: JS lacks named-parameter introspection. By default, all positional args go to { args: [...] }. Provide a toolArgs mapper to get cleaner audit logs.

Errors

import {
  PliuzError,                     // base
  PliuzApiError,                  // any 4xx/5xx from Pliuz API
  PliuzAuthError,                 // 401 — bad/missing key
  PliuzForbiddenError,            // 403 — agent mismatch
  PliuzNotFoundError,             // 404
  PliuzConflictError,             // 409 — duplicate execution report
  PliuzValidationError,           // 400 — invalid body
  PliuzPolicyError,               // 422 — no policy matched
  PliuzRateLimitError,            // 429
  PliuzServerError,               // 5xx
  PliuzNetworkError,              // connection failed
  PliuzTimeoutError,              // request timed out
  PliuzRejectedError,             // human said no
  PliuzApprovalExpiredError,      // SLA expired before human decided
  PliuzApprovalTimeoutError,      // SDK polling gave up
} from '@pliuz/sdk'

try {
  await issueRefund(...)
} catch (e) {
  if (e instanceof PliuzRejectedError) {
    console.log('Human rejected:', e.reason)
    return
  }
  if (e instanceof PliuzApprovalTimeoutError) {
    // Poll later via client.getApproval(e.approvalId)
  }
  throw e
}

Low-level client

import { PliuzClient } from '@pliuz/sdk'

const pliuz = new PliuzClient()  // reads PLIUZ_API_KEY

const created = await pliuz.createApproval({
  tool_name: 'refund',
  tool_args: { amount: 100 },
  idempotency_key: 'abc-123',
})

const fetched = await pliuz.getApproval(created.id)

await pliuz.reportExecution(created.id, {
  status: 'success',
  latency_ms: 42,
})

Redaction

import { applyRedaction } from '@pliuz/sdk'

const clean = applyRedaction(
  { customer: { ssn: '123-45-6789', id: 'cus_x' } },
  ['customer.ssn'],
)
// { customer: { ssn: '<redacted>', id: 'cus_x' } }

Supports dotted paths and [*] for array wildcards:

applyRedaction({ items: [{ card: '...' }] }, ['items[*].card'])

Input is never mutated — returns a deep clone.

Vercel AI SDK integration (optional)

npm install ai
import { tool, generateText } from 'ai'
import { z } from 'zod'
import { gatedTool } from '@pliuz/sdk/adapters/ai'

const issueRefund = gatedTool(
  { toolName: 'issue_refund', policy: 'refund', redact: ['customer.ssn'] },
  tool({
    description: 'Issue a refund to a customer',
    inputSchema: z.object({
      customer_id: z.string(),
      amount_cents: z.number().int().positive(),
    }),
    execute: async ({ customer_id, amount_cents }) =>
      stripe.refunds.create({ customer: customer_id, amount: amount_cents }),
  }),
)

const { text } = await generateText({
  model: openai('gpt-4o'),
  tools: { issueRefund },
  prompt: 'Refund cus_123 for $50',
})

The wrapped tool preserves description and the input schema (inputSchema in ai v5/v6, parameters in v4) — the LLM sees it identically. Only the execution path is gated.

Claude Agent SDK integration (optional)

npm install @anthropic-ai/claude-agent-sdk

gatedTool() wraps tool(...) from the Claude Agent SDK, preserving name, description, and inputSchema. toolName defaults to the tool's own name.

import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'
import { z } from 'zod'
import { gatedTool } from '@pliuz/sdk/adapters/claude-agent'

const issueRefund = gatedTool(
  { policy: 'refund', redact: ['customer_id'] },
  tool(
    'issue_refund',
    'Issue a refund to a customer',
    { customer_id: z.string(), amount_cents: z.number().int().positive() },
    async ({ customer_id, amount_cents }) => ({
      content: [{ type: 'text', text: await stripe.refund(customer_id, amount_cents) }],
    }),
  ),
)

const server = createSdkMcpServer({ name: 'billing', version: '1.0.0', tools: [issueRefund] })

OpenAI Agents SDK integration (optional)

npm install @openai/agents

gatedTool() wraps tool({...}) from @openai/agents, preserving name, description, and parameters. The SDK invokes tools with a JSON-string payload and a live RunContext; the adapter parses the JSON so top-level redact paths match, and the RunContext never enters the audit payload or the idempotency key.

import { Agent, tool } from '@openai/agents'
import { z } from 'zod'
import { gatedTool } from '@pliuz/sdk/adapters/openai-agents'

const issueRefund = gatedTool(
  { policy: 'refund', redact: ['customer_id'] },
  tool({
    name: 'issue_refund',
    description: 'Issue a refund to a customer',
    parameters: z.object({ customer_id: z.string(), amount_cents: z.number().int() }),
    execute: async ({ customer_id, amount_cents }) => stripe.refund(customer_id, amount_cents),
  }),
)

const agent = new Agent({ name: 'Billing', instructions: '...', tools: [issueRefund] })

@openai/agents also ships a built-in needsApproval flag, but it only pauses the local run loop — no external routing, audit trail, or approver UI. Use this adapter when approval must leave the process and be recorded in Pliuz's hash-chained audit log.

Management API (ManagementClient)

gated() and PliuzClient use a per-agent key to request approvals. To manage Pliuz as code — policies, tools, agents, users — and to pull the signed audit trail for your SIEM, use ManagementClient. It authenticates with a machine token (plm_live_...) via Authorization: Bearer, read from PLIUZ_MANAGEMENT_KEY (separate from PLIUZ_API_KEY). Built for CI/CD, IaC (Terraform), and SIEM automation; scopes are enforced server-side, so a read-only token gets PliuzForbiddenError on writes.

import { ManagementClient } from '@pliuz/sdk'

const mgmt = new ManagementClient() // reads PLIUZ_MANAGEMENT_KEY

// Policy-as-code
const policy = await mgmt.createPolicy({
  name: 'refunds-over-1000',
  priority: 10,
  conditions: { '>': [{ var: 'amount_cents' }, 100000] },
  approver_group: 'finance',
  sla_seconds: 3600,
  enabled: true,
})
const { policies } = (await mgmt.listPolicies({ enabledOnly: true })) as {
  policies: Array<{ name: string }>
}

// Audit trail — verify the hash chain and pull a signed export for your SIEM
const { verified } = (await mgmt.verifyChain()) as { verified: boolean }
const exported = await mgmt.exportEvents({ limit: 1000 })
// exported.jsonl is Ed25519-signed; exported.headers carry the x-pliuz-export-* proofs

All methods are async and typed:

| Surface | Methods | |---|---| | Policies | listPolicies, getPolicy, createPolicy, updatePolicy, disablePolicy | | Tools | listTools, getTool, createTool, updateTool | | Agents | listAgents, createAgent | | Users | listUsers, createUser, archiveUser | | Audit trail | listEvents, verifyChain, getChainHead, listAnchors, exportEvents |

The contract is published as management-openapi.yaml. All env vars can be passed as constructor options:

new ManagementClient({ token: 'plm_live_...', baseUrl: 'https://pliuz.mycompany.com' })

Configuration

| Env var | Default | Purpose | |---|---|---| | PLIUZ_API_KEY | (required) | Per-agent API key. pli_live_... format. | | PLIUZ_MANAGEMENT_KEY | (required for ManagementClient) | Machine token for the Management API. plm_live_... format. Separate from PLIUZ_API_KEY. | | PLIUZ_BASE_URL | https://pliuz.com | Override for self-hosted or staging. |

All env vars can be passed as PliuzClient constructor options:

new PliuzClient({ apiKey: 'pli_live_...', baseUrl: 'https://pliuz.mycompany.com' })

Production tips

Idempotency — read this before relying on it

gated() automatically generates a deterministic idempotency_key per call from (tool_name, args, sessionId). The key is hashed over the pre-redaction args (only the truncated SHA-256 leaves your process), so two calls that differ only in a redacted field get different keys — each needs its own approval.

Important: the backend dedupes on this key within a 24-hour window, scoped to the tenant and originating agent. An identical (tool_name, args, sessionId) combination replays the same approval during that window, which makes HTTP retries safe. Reusing the same idempotency_key within 24 hours with different tool_args is rejected as an idempotency conflict. After the window expires, the same key can create a new approval.

With the default sessionId: undefined, repeated identical calls from the same agent can still replay for up to 24 hours. If your agent legitimately repeats identical calls (crons, recurring jobs), set a per-run sessionId so each run gets its own approval:

const refund = gated(
  { policy: 'refund', sessionId: runId },  // scope approvals to this run
  async (id: string, cents: number) => { /* ... */ },
)

Within one run, retries are then safe: same args → one approval request, one human decision.

Cross-language idempotency

The hash algorithm matches the Python SDK for ASCII string/integer payloads. Known divergence: non-ASCII strings and float-typed numbers serialize differently between JSON.stringify and Python's json.dumps, producing different keys — don't rely on cross-language dedupe for those payloads yet.

When the approver edits the args

If a human decides with Edit & Approve, gated() executes the edited final_args, never the original call. With the default toolArgs mapper this is automatic. With a custom toolArgs mapper, provide applyFinalArgs to map the edited object back onto your function's arguments — otherwise the SDK throws PliuzEditNotApplicableError (it refuses to run args the human did not approve):

const refund = gated(
  {
    toolArgs: (id: string, cents: number) => ({ customer_id: id, amount_cents: cents }),
    applyFinalArgs: (fa, [id]) => [id, fa.amount_cents as number] as const,
  },
  async (id: string, cents: number) => { /* ... */ },
)

Decision latency (long-poll)

gated() waits for the human decision via server long-poll: each wait call holds the connection open up to ~25s and returns the instant the approval is decided. You get near-zero decision-delivery latency and ~12x fewer requests than fixed-interval polling — no configuration needed. pollIntervalMs is only the safety-floor backoff used if the server ignores the wait hint (it does NOT shorten the long-poll window). The low-level client exposes it directly:

const a = await pliuz.getApproval(id, 25) // long-poll up to 25s

Custom timeouts per call

  • Live user-facing: timeoutMs: 30_000
  • Background jobs: timeoutMs: 86_400_000 (24 h)
  • Critical security gates: don't use gated() — use a synchronous Pliuz dashboard flow

When gated() rejects mid-execution

If polling exceeds timeoutMs, you get PliuzApprovalTimeoutError. Your wrapped function never runs. The approval may still resolve later server-side — handle the retry with pliuz.getApproval(e.approvalId).

Edge / Bun / Deno

The HTTP layer uses native fetch + AbortController, but gated() imports node:crypto at module load — so today the SDK requires a Node-compatible runtime: Node ≥ 18.17, Bun, Deno (via npm:), and Cloudflare Workers with nodejs_compat. Vercel Edge runtime is NOT supported yet (no node:crypto there); use the Node.js runtime for routes that import this SDK. A Web-Crypto build is on the roadmap.


Versioning

This SDK follows SemVer. The current line is 0.x.y — minor versions may include breaking changes until 1.0.0.

| Surface | Stability | |---|---| | gated, PliuzClient | Stable across 0.x patches | | errors.* | Stable across 0.x patches | | adapters/* | Experimental — may shift before 1.0 |


Examples

See examples/ for runnable scripts:


Links

  • Docs: https://pliuz.com/docs
  • GitHub: https://github.com/pliuz/pliuz-ts
  • Issues: https://github.com/pliuz/pliuz-ts/issues
  • Python SDK: pliuz

License

Apache 2.0. The Pliuz platform itself is proprietary — this SDK is the public client only.