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

@saltfish-ai/cli

v0.1.5

Published

Saltfish CLI & SDK — programmatic access to the Saltfish platform

Readme

@saltfish-ai/cli

The official Saltfish CLI and TypeScript SDK. Manage flows, demos, analytics, and A/B tests from the command line or programmatically in your own code.

Installation

npm install @saltfish-ai/cli

Or install globally for CLI access:

npm install -g @saltfish-ai/cli

Quickstart

CLI

# Authenticate with your API key
saltfish auth login sk_live_abc123

# List your flows
saltfish flows list

# Get a flow with expanded nodes
saltfish flows get FLOW_ID --expand nodes,edges

# Deploy a flow to live
saltfish flows deploy FLOW_ID

# View analytics
saltfish analytics overview --start 2026-01-01

SDK (TypeScript)

import { SaltfishClient } from '@saltfish-ai/cli';

const saltfish = new SaltfishClient({ apiKey: 'sk_live_...' });

// List all flows
const { data: flows } = await saltfish.flows.list();

// Get a flow with expanded nodes and edges
const { data: flow } = await saltfish.flows.get('flow_id', {
  expand: ['nodes', 'edges'],
});

// Create and deploy a flow
const { data: newFlow } = await saltfish.flows.create({ title: 'Onboarding' });
await saltfish.flows.deploy(newFlow.id);

Authentication

API keys are created in the Saltfish dashboard:

  1. Go to app.saltfish.aiSettingsDeveloper
  2. Click Create API Key
  3. Copy the key (shown once) — it starts with sk_live_ or sk_test_

CLI auth

# Save your API key (verified on save)
saltfish auth login sk_live_abc123

# Check current config
saltfish auth status

# Remove stored credentials
saltfish auth logout

SDK auth

const saltfish = new SaltfishClient({
  apiKey: 'sk_live_abc123',
});

Environment variables

| Variable | Description | |----------|-------------| | SALTFISH_API_KEY | API key (overrides stored key) | | SALTFISH_API_URL | API base URL (default: https://studio-api.saltfish.ai/studio/api) |

Environment variables take precedence over stored config, making it easy to use in CI/CD:

SALTFISH_API_KEY=sk_live_abc123 saltfish flows list

SDK Reference

Client configuration

import { SaltfishClient } from '@saltfish-ai/cli';

const saltfish = new SaltfishClient({
  apiKey: 'sk_live_...',         // Required
  baseUrl: 'https://...',        // Optional — custom API endpoint
  maxRetries: 2,                 // Optional — retries on 5xx/network errors (default: 2)
  timeout: 30000,                // Optional — request timeout in ms (default: 30000)
});

Resource namespaces

The client exposes four resource namespaces:

| Namespace | Description | |-----------|-------------| | saltfish.flows | Interactive demo flows | | saltfish.demos | Demo recordings | | saltfish.analytics | Engagement analytics | | saltfish.abTests | A/B test management |

Flows

// List flows (with optional filters)
const flows = await saltfish.flows.list({ status: 'live', limit: 10 });

// Get a single flow
const flow = await saltfish.flows.get('flow_id');

// Get with expanded resources
const flow = await saltfish.flows.get('flow_id', { expand: ['nodes', 'edges'] });

// Create a flow
const flow = await saltfish.flows.create({ title: 'My Flow' });

// Update a flow
const flow = await saltfish.flows.update('flow_id', { title: 'New Title' });

// Deploy to live
await saltfish.flows.deploy('flow_id');

// Unpublish
await saltfish.flows.unpublish('flow_id');

// Delete (soft delete)
await saltfish.flows.delete('flow_id');

Demos

// List demos (with optional folder filter)
const demos = await saltfish.demos.list({ folderId: 'folder_id', limit: 10 });

// Get a demo with steps expanded
const demo = await saltfish.demos.get('demo_id', { expand: ['steps'] });

// Update metadata
await saltfish.demos.update('demo_id', { title: 'New Title', folderId: 'folder_id' });

// Duplicate
const copy = await saltfish.demos.duplicate('demo_id');

// Get shareable link
const { data: { url } } = await saltfish.demos.getShareLink('demo_id');

// Delete (soft delete)
await saltfish.demos.delete('demo_id');

Analytics

// Overview — all flows and demos
const overview = await saltfish.analytics.overview({
  startDate: '2026-01-01',
  endDate: '2026-03-31',
});

// Per-flow analytics
const flowStats = await saltfish.analytics.flow('flow_id', {
  startDate: '2026-01-01',
});

// Per-demo analytics
const demoStats = await saltfish.analytics.demo('demo_id');

A/B Tests

// List all A/B tests
const tests = await saltfish.abTests.list();

// Get users in a userList test
const { userIds, count } = await saltfish.abTests.getUsers('test_id');

// Add users to a test
await saltfish.abTests.addUsers('test_id', ['[email protected]', '[email protected]']);

// Remove users from a test
await saltfish.abTests.removeUsers('test_id', ['[email protected]']);

CLI Reference

saltfish auth

| Command | Description | |---------|-------------| | saltfish auth login <api-key> | Save and verify your API key | | saltfish auth status | Show current config | | saltfish auth set-url <url> | Point CLI at a custom API endpoint | | saltfish auth logout | Remove stored credentials |

saltfish flows

| Command | Description | |---------|-------------| | saltfish flows list | List all flows | | saltfish flows get <id> | Get flow details | | saltfish flows create -t "Title" | Create a new flow | | saltfish flows update <id> -t "Title" | Update flow metadata | | saltfish flows deploy <id> | Deploy flow to live | | saltfish flows unpublish <id> | Take a flow offline | | saltfish flows delete <id> | Soft-delete a flow |

Options:

--status <live|draft>    Filter by status (list only)
--expand <fields>        Expand nested resources: nodes, edges (get only)
--limit <n>              Max results (default: 25)
--cursor <cursor>        Pagination cursor
-o, --output <format>    Output format: json | table (default: table)

saltfish demos

| Command | Description | |---------|-------------| | saltfish demos list | List all demo recordings | | saltfish demos get <id> | Get demo details | | saltfish demos update <id> -t "Title" | Update demo metadata | | saltfish demos duplicate <id> | Duplicate a demo | | saltfish demos share <id> | Get shareable link | | saltfish demos delete <id> | Soft-delete a demo |

Options:

--folder <folderId>      Filter by folder (list only), use "none" to unset (update)
--expand <fields>        Expand nested resources: steps (get only)
--limit <n>              Max results (default: 25)
--cursor <cursor>        Pagination cursor
-o, --output <format>    Output format: json | table (default: table)

saltfish analytics

| Command | Description | |---------|-------------| | saltfish analytics overview | Aggregated engagement metrics | | saltfish analytics flow <id> | Per-flow analytics | | saltfish analytics demo <id> | Per-demo analytics |

Options:

--start <YYYY-MM-DD>     Start date filter
--end <YYYY-MM-DD>       End date filter
-o, --output <format>    Output format: json | table (default: table)

Pagination

All list endpoints use cursor-based pagination:

CLI

# First page
saltfish flows list --limit 10

# Next page (use the last ID from the previous response)
saltfish flows list --limit 10 --cursor LAST_FLOW_ID

SDK

// First page
const page1 = await saltfish.flows.list({ limit: 10 });
console.log(page1.data);       // Flow[]
console.log(page1.hasMore);    // true
console.log(page1.totalCount); // 42

// Next page
if (page1.hasMore) {
  const lastId = page1.data[page1.data.length - 1].id;
  const page2 = await saltfish.flows.list({ limit: 10, cursor: lastId });
}

Expandable Resources

By default, nested resources are omitted to keep responses fast. Use expand to include them:

CLI

# Include node data in flow response
saltfish flows get FLOW_ID --expand nodes,edges

# Include step data in demo response
saltfish demos get DEMO_ID --expand steps

SDK

const flow = await saltfish.flows.get('flow_id', {
  expand: ['nodes', 'edges'],
});

// flow.data.nodes is now populated
// flow.data.edges is now populated

| Resource | Expandable fields | |----------|-------------------| | Flow | nodes, edges | | Demo | steps |

Error Handling

The API returns structured errors:

{
  "error": {
    "type": "not_found",
    "message": "Flow xyz not found",
    "code": "resource_not_found"
  },
  "requestId": "req_abc123"
}

SDK error handling

import { SaltfishClient, SaltfishApiError } from '@saltfish-ai/cli';

try {
  await saltfish.flows.get('nonexistent');
} catch (err) {
  if (err instanceof SaltfishApiError) {
    console.log(err.status);    // 404
    console.log(err.type);      // "not_found"
    console.log(err.code);      // "resource_not_found"
    console.log(err.detail);    // "Flow nonexistent not found"
    console.log(err.requestId); // "req_abc123"
    console.log(err.param);     // undefined (set for validation errors)
  }
}

Error types

| Type | Description | |------|-------------| | invalid_request | Malformed request, missing required fields, invalid parameters | | authentication_error | Invalid or missing API key | | not_found | Resource doesn't exist or isn't accessible | | rate_limit | Too many requests — back off and retry | | api_error | Internal server error |

Rate Limiting

The API enforces rate limits per API key (default: 100 requests/minute). Every response includes rate limit headers:

| Header | Description | |--------|-------------| | X-RateLimit-Limit | Max requests per window | | X-RateLimit-Remaining | Requests remaining in current window | | X-RateLimit-Reset | Unix timestamp when the window resets |

The SDK handles rate limiting automatically — when a 429 response is received, it waits for the Retry-After duration and retries the request (up to maxRetries).

// Access rate limit info from any response
const response = await saltfish.flows.list();
console.log(response._rateLimit);
// { limit: 100, remaining: 97, reset: 1741564800 }

Retries

The SDK automatically retries on:

  • 5xx errors — server errors
  • 429 errors — rate limit exceeded (waits for Retry-After)
  • Network errors — connection failures, timeouts

Retries use exponential backoff (500ms → 1s → 2s, capped at 5s). Client errors (4xx, except 429) are never retried.

const saltfish = new SaltfishClient({
  apiKey: 'sk_live_...',
  maxRetries: 3,    // Default: 2
  timeout: 60000,   // Default: 30000ms
});

Response Format

Single object

{
  "object": "flow",
  "requestId": "req_abc123",
  "data": {
    "id": "abc123",
    "title": "Onboarding Flow",
    "status": "live",
    "nodesCount": 5,
    "deployedAt": "2026-03-01T12:00:00Z",
    "createdAt": "2026-02-15T10:00:00Z",
    "updatedAt": "2026-03-01T12:00:00Z"
  }
}

List

{
  "object": "list",
  "requestId": "req_def456",
  "data": [
    { "id": "abc123", "title": "Onboarding Flow", "status": "live", "..." : "..." },
    { "id": "def456", "title": "Product Tour", "status": "draft", "..." : "..." }
  ],
  "hasMore": true,
  "totalCount": 42
}

Error

{
  "error": {
    "type": "not_found",
    "message": "Flow xyz not found",
    "code": "resource_not_found"
  },
  "requestId": "req_ghi789"
}

Idempotency

Mutation endpoints accept an Idempotency-Key header to safely retry requests without duplicate side effects:

// SDK — pass idempotency key to create operations
const flow = await saltfish.flows.create(
  { title: 'My Flow' },
  'unique-key-12345'  // idempotency key
);
# cURL — include the header directly
curl -X POST https://studio-api.saltfish.ai/studio/api/v1/flows \
  -H "X-API-Key: sk_live_abc123" \
  -H "Idempotency-Key: unique-key-12345" \
  -H "Content-Type: application/json" \
  -d '{"title": "My Flow"}'

API Documentation

Interactive API docs (Swagger UI) are available at:

https://studio-api.saltfish.ai/studio/api/v1/docs

License

MIT