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

maritime-sdk

v0.7.1

Published

Official TypeScript SDK for Maritime: provision and drive AI agents on Maritime's serverless infrastructure from your own backend.

Readme

maritime-sdk

Official TypeScript SDK for Maritime: provision and drive AI agents on Maritime's serverless infrastructure, straight from your own backend.

Build an app where every one of your users gets their own agent: one call on sign-up spins up an isolated agent on Maritime's fleet; another sends it a message. You never touch a container.

npm install maritime-sdk

Requires Node 18+ (or any runtime with a global fetch: Bun, Deno, edge). Zero runtime dependencies.

Quick start

import { Maritime } from 'maritime-sdk'

const maritime = new Maritime({ apiKey: process.env.MARITIME_API_KEY })

// When YOUR user signs up, give them their own agent. `provision` is
// idempotent on externalId, so it's safe to call on every login.
const agent = await maritime.agents.provision({
  externalId: `customer_${user.id}`,   // your id for this agent
  name: `assistant-${user.id}`,
  template: 'openclaw',
})

// Talk to it (sleeping agents auto-wake):
const { response } = await maritime.agents.chat(agent.id, 'Summarize my unread email.')
console.log(response)

Authentication

Mint an API key (mk_...) from the dashboard (Settings → API keys) or the CLI (maritime keys create), then:

const maritime = new Maritime({ apiKey: 'mk_...' })
// or set MARITIME_API_KEY and call new Maritime()

Keys carry scopes: hand a narrower key to a subsystem that only needs part of the surface:

| Scope | Grants | |-------|--------| | provision | create agents | | deploy | start/stop/restart/sleep/chat | | secrets | read/write env vars | | manage | everything, incl. delete + key management (wildcard) |

const worker = await maritime.keys.create({ name: 'chat-worker', scopes: ['deploy'] })
// worker.rawKey is shown once, so store it now.

Keys can also be project-scoped: the key to put in a partner-facing or per-tenant service. A scoped key only works on the front door + usage endpoints for that one project and is rejected everywhere else (other projects 404), so a leak means "message your own fleet", not your wallet:

const runtime = await maritime.keys.create({
  name: 'poke-runtime',
  scopes: ['deploy', 'manage'],
  projectId: agent.projectId as string,
})

Agents

// Create (kicks off deploy). Prefer `template`: a bare framework yields a broken image.
const agent = await maritime.agents.create({
  name: 'support-bot',
  template: 'openclaw',
  externalId: 'customer_42',
  instructions: 'You are a friendly support agent for Acme Inc.',
  env: [{ key: 'ACME_API_KEY', value: '...', secret: true }],
  idleTtlSeconds: 3600,     // 0 = always-on
})

// Idempotent get-or-create by externalId (recommended for per-user provisioning)
const agent = await maritime.agents.provision({ externalId: 'customer_42', name: 'support-bot' })

await maritime.agents.get(agent.id)
await maritime.agents.list({ externalId: 'customer_42' })  // filter by your id
await maritime.agents.list()                                // all of your agents

// Chat (synchronous; auto-wakes a sleeping agent)
const { response, error } = await maritime.agents.chat(agent.id, 'Hello')

// Lifecycle
await maritime.agents.start(agent.id)
await maritime.agents.sleep(agent.id)     // cheapest resting state (serverless snapshot)
await maritime.agents.restart(agent.id)
await maritime.agents.delete(agent.id)    // tears down container + volume + network

// Env vars (secrets encrypted at rest; reach a running container after reloadEnv)
await maritime.agents.setEnv(agent.id, 'STRIPE_KEY', 'sk_live_...', { secret: true })
await maritime.agents.listEnv(agent.id)
await maritime.agents.reloadEnv(agent.id)

// Logs
await maritime.agents.logs(agent.id, { limit: 100, level: 'error' })

// Real-world identity (Identity add-on agents): phone, email, tunnel host.
// Channels never provisioned come back null; a fresh phone number reports
// smsStatus 'pending' during carrier warm-up.
const { phoneNumber, emailAddress } = await maritime.agents.identity(agent.id)

// Skills: list, search ClawHub, install, upload a custom skill, export.
// Install/upload auto-resolve missing binaries inside the agent's own VM
// and report the final state; `needs_input` means the skill just needs
// the listed env values (an API key, say) from your user.
const overview = await maritime.agents.skills.list(agent.id)
const result = await maritime.agents.skills.upload(agent.id, {
  archive: fs.readFileSync('daily-brief.zip'),   // zip or a bare SKILL.md
})
if (result.status === 'needs_input') {
  await maritime.agents.skills.setEnv(agent.id, { SOME_API_KEY: value })
}
const tarball = await maritime.agents.skills.export(agent.id, result.name)
await maritime.agents.skills.remove(agent.id, result.name)

Projects: one agent per end-user (the front door)

Route your end-users' messages through a project: each externalUserId gets a sticky binding to its own agent (spawned from your template, warm-pool backed). Full guide: docs/FRONT_DOOR.md.

// One-time: fleet policy on the agent's auto-created project
await maritime.projects.update(agent.projectId as string, {
  newChatPolicy: 'spawn',   // dedicated agent per end-user
  warmPoolSize: 3,          // new users bind in ~1s instead of a cold create
})

// Per message: get-or-create + wake + deliver
const msg = await maritime.projects.message(agent.projectId as string, {
  externalUserId: `user_${theirUserId}`,
  message: 'What does my dashboard say?',
  idempotencyKey: requestId,          // retries never double-deliver
})
if (msg.status === 'replied') console.log(msg.reply)
// 'provisioning' | 'queued' → reply arrives via the message.reply webhook,
// or poll: await maritime.projects.getMessage(projectId, msg.messageId)

// Your customer list
await maritime.projects.users(projectId)
await maritime.projects.unbindUser(projectId, 'user_42')

Subscribe to replies (and verify deliveries) with the webhook helper:

import { verifyWebhookSignature } from 'maritime-sdk'

const { secret } = await maritime.webhooks.create({
  url: 'https://api.acme.co/maritime/webhook',
  events: ['message.reply', 'message.failed'],
})

// in your handler, pass the RAW body:
const ok = await verifyWebhookSignature({ secret, body: rawBody, signature: sigHeader })

Billing: rebill your users, keep the wallet alive

// Per-agent / per-end-user cost over a date range (max 92 days), from the
// real money ledger. Rows carry externalUserId, so invoicing is a one-liner.
const report = await maritime.billing.usage({ from: '2026-07-01', to: '2026-08-01' })
for (const row of report.agents) invoice(row.externalUserId, row.totalCostCents)

// Auto-recharge: below thresholdCents, Maritime charges your saved card for
// amountCents (max 3x/24h, 6h backoff after a failure). Kills the "wallet
// hits zero and the whole fleet sleeps" cliff.
await maritime.billing.setAutoRecharge({ enabled: true, thresholdCents: 2000, amountCents: 10000 })

// Per-end-user compute caps: stamped on every NEW instance the project
// spawns; over-cap instances auto-sleep instead of draining the wallet.
await maritime.projects.update(projectId, { instanceComputeMinutesLimit: 300 })

Scheduled wakes (agent-side)

These helpers run INSIDE a Maritime agent (a BYO image speaking the contract). A Maritime micro-VM sleeps at ~zero cost between messages, so a plain setInterval never fires while it's snapshotted; instead, publish your schedule and Maritime wakes the VM at the right moments.

import { observeScheduler, pushSchedules } from 'maritime-sdk'

// One line: every add/remove on your scheduler re-publishes the snapshot.
observeScheduler(myScheduler, {
  getSnapshot: (s) => s.jobs.map(j => ({ id: j.id, nextRunAt: j.next.toISOString() })),
})

// Or push explicitly (send the FULL list; [] clears all synced wakes):
await pushSchedules([
  { id: 'digest', cron: '0 9 * * 1-5', tz: 'America/New_York', prompt: 'Send the digest' },
  { id: 'followup', nextRunAt: '2026-08-01T14:00:00Z' },
])

Entries use nextRunAt (the next occurrence as YOUR scheduler computed it) or a 5-field cron + tz. An entry with prompt gets it delivered to your POST /chat (source "scheduled") right after the wake. Credentials come from the env every Maritime agent already has; off-Maritime both helpers are silent no-ops, so they're safe in local dev. Alternative with no SDK at all: serve GET /schedules returning the same array and Maritime polls it.

Errors

Every failure is a subclass of MaritimeError: catch the base to catch them all, or narrow by type:

import {
  MaritimeAuthError,           // 401 / 403: bad or under-scoped key
  MaritimePaymentRequiredError,// 402: wallet needs funding
  MaritimeNotFoundError,       // 404: no such agent (or not yours)
  MaritimeConflictError,       // 409: name already taken
  MaritimeRateLimitError,      // 429
  MaritimeAPIError,            // any other non-2xx (has .status, .detail)
  MaritimeConnectionError,     // never reached Maritime (network/timeout)
} from 'maritime-sdk'

try {
  await maritime.agents.create({ name: 'dupe', template: 'openclaw' })
} catch (err) {
  if (err instanceof MaritimeConflictError) {
    // an agent with that name already exists
  } else if (err instanceof MaritimeAPIError) {
    console.error(err.status, err.detail, err.requestId)
  }
}

Configuration

new Maritime({
  apiKey: 'mk_...',                     // or MARITIME_API_KEY
  baseUrl: 'https://api.maritime.sh',   // or MARITIME_API_URL
  timeout: 60_000,                      // per-request ms
  maxRetries: 2,                        // network + 5xx/429 (GET/DELETE and 429/503 only)
  fetch: customFetch,                   // inject a fetch (tests, proxies)
  defaultHeaders: { 'x-team': 'acme' },
})

Retries are safe by construction: GET/DELETE retry on any transient failure; POST/PUT retry only on network errors and 429/503 (never a 5xx that might have applied a write).

License

MIT