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

@bitclub.ai/opendesk-server-sdk

v0.0.2

Published

OpenDesk Server Node.js SDK - JavaScript client for OpenDesk API

Readme

@opendesk/sdk

OpenDesk Server Node.js SDK - JavaScript/TypeScript client for OpenDesk API.

Features

  • Full API Coverage - 90+ endpoints across 9 modules (auth / namespaces / skills / review / labels / teams / admin / users / relay)
  • Type Safe - Written in TypeScript with full type definitions
  • Dual Auth - JWT login (auto-applied) and API Tokens / Relay Keys (sk_xxx, od_xxx)
  • Timeout & Retry - Configurable request timeout, idempotent-request retry with exponential backoff
  • Cancellation - AbortSignal passthrough for every request
  • Interceptors - onRequest / onResponse hooks, custom fetch injection
  • Error Handling - Typed error classes; parses both FastAPI {detail} and OpenAI-style {"error":{message,code}} bodies
  • Pagination - Built-in async generators for automatic page iteration
  • Zero Dependencies - Uses native fetch API (Node.js >= 18) and works in browsers
  • Download - Files as Blob (browser and Node.js >= 18) or raw ArrayBuffer

Installation

npm install @opendesk/sdk

Quick Start

import { OpenDeskClient } from '@opendesk/sdk';

const client = new OpenDeskClient({
  baseUrl: 'http://127.0.0.1:8000/api/v1',
});

// Login - automatically sets the token for subsequent requests
await client.auth.login({ username: 'admin', password: 'admin' });

// Get current user
const me = await client.auth.getMe();
console.log('Hello,', me.username);

// List namespaces
const namespaces = await client.namespaces.list();
console.log('Namespaces:', namespaces.items);

Authentication

JWT Login (Session-based)

const client = new OpenDeskClient({ baseUrl: 'http://127.0.0.1:8000/api/v1' });

// Login - token is stored on the client automatically
await client.auth.login({ username: 'your-user', password: 'your-pass' });
await client.auth.getMe(); // Authorization header sent automatically

// Refresh returns a new token WITHOUT auto-applying it:
const token = await client.auth.refresh();
client.setToken(token.access_token); // rotate manually

// Logout - revokes the current JWT
await client.auth.logout();

API Token (Server-to-Server)

// Initialize with API Token or Relay Key
const client = new OpenDeskClient({
  baseUrl: 'http://127.0.0.1:8000/api/v1',
  token: 'sk_your_api_token_here', // or od_ relay key
});

// All requests use the token automatically
const skills = await client.skills.catalog();

Managing API Tokens

// List existing tokens
const tokens = await client.auth.listTokens();

// Create a new token
const newToken = await client.auth.createToken({
  name: 'CI Pipeline',
  expires_in_days: 90,
  scopes: ['skill:read', 'skill:publish'],
});
console.log('Token:', newToken.access_token); // Save this - shown only once!

// Delete a token
await client.auth.deleteToken(newToken.id);

Casdoor SSO & OAuth

// Is Casdoor enabled?
const status = await client.auth.getCasdoorStatus();

// Browser redirect flow:
const { login_url } = await client.auth.getCasdoorLoginUrl();
window.location.href = login_url;

// Handle the callback (code + state from the query string)
// On success the token is applied to the client automatically:
await client.auth.casdoorCallback(code, state);

// Account management
await client.auth.register({ username, email, password }); // Casdoor signup
await client.auth.updateMe({ display_name: 'New Name' });  // update profile
const accounts = await client.auth.listOAuthAccounts();    // bound OAuth accounts
await client.auth.unlinkOAuthAccount('casdoor');           // unbind (400 when it is the only provider)

API Modules

Namespaces (Teams)

// Create a namespace
const ns = await client.namespaces.create({
  slug: 'my-team',
  name: 'My Team',
  is_public: false,
});

// Members
await client.namespaces.addMember('my-team', { user_id: 42, role: 'ADMIN' });
const members = await client.namespaces.listMembers('my-team');

// Invitations
const inv = await client.namespaces.createInvitation('my-team', { role: 'MEMBER', expires_in_days: 7 });
const invitations = await client.namespaces.listInvitations('my-team');
await client.namespaces.revokeInvitation('my-team', inv.id);
await client.namespaces.acceptInvitation(inv.token); // join as member

Skills

// Search skills
const results = await client.skills.search({ q: 'ai', page_size: 20 });

// Publish a skill (two-step: upload then publish)
const upload = await client.skills.upload(formData); // FormData with .zip file
const published = await client.skills.publish({
  namespace: 'my-team',
  slug: 'my-skill',
  name: 'My Skill',
  version: '1.0.0',
  upload_token: upload.upload_token,
});

// Ownership transfer / copy
await client.skills.transfer('my-team', 'my-skill', { target_namespace: 'other', target_user_id: 42 });
await client.skills.copy('my-team', 'my-skill', { target_namespace: 'other' });

// Download a skill
const blob = await client.skills.download('my-team', 'my-skill', '1.0.0');

Review (Governance)

// List pending reviews / my todos
const pending = await client.review.pending({ namespace: 'my-team' });
const todos = await client.review.myTodos();

// Approve / reject / promote
await client.review.approve('publish-id', { reason: 'Looks good!' });
await client.review.reject('publish-id', { reason: 'Needs fixes' });
await client.review.promoteGlobal('publish-id', { reason: 'Excellent' });

// Skill change requests
const requests = await client.review.changeRequests({ status: 'PENDING' });
await client.review.approveChangeRequest('request-id');

// Super admin: audit log & promotion candidates
await client.review.auditLog({ action: 'APPROVE' });
await client.review.promotionCandidates();

Labels

const labels = await client.labels.list();          // all labels
await client.labels.create({ name: 'AI', category: 'tech' });

Teams (Join Requests + Notifications)

await client.teams.applyJoin('my-namespace');                        // apply to join
const requests = await client.teams.listJoinRequests('my-namespace');
await client.teams.approveJoin('my-namespace', 'request-id');
await client.teams.rejectJoin('my-namespace', 'request-id');

const notifs = await client.teams.listNotifications();
await client.teams.markRead('notification-id');
await client.teams.markAllRead();

Admin (Super Admin Only)

// Operation logs
const logs = await client.admin.listLogs({ source: 'opendesk' });
const csvBlob = await client.admin.exportLogs({ action: 'approve' });

// Namespace quota
await client.admin.updateNamespaceQuota('my-team', { max_members: 50, max_skills: 100 });

// RBAC (Casdoor)
const permissions = await client.admin.listPermissions();
const roles = await client.admin.listRoles();
await client.admin.createRole('admin', 'my-team');               // query params
await client.admin.deleteRole('admin');
await client.admin.listUserRoles(42);                            // by user id
await client.admin.assignRole({ username: 'alice', role: 'owner', namespace_slug: 'my-team' });
await client.admin.removeRole('alice', 'owner', 'my-team');

// Skill import (skillhub.cn)
const preview = await client.admin.previewImportSkills({ limit: 10 });
await client.admin.importSkills(preview.items);

Users

const profile = await client.users.getProfile('username');

const followers = await client.users.getFollowers('username');   // { items: [...] }, non-paginated
const following = await client.users.getFollowing('username');

await client.users.follow('username');
await client.users.unfollow('username');

const dataBlob = await client.users.exportMyData(); // GDPR export (ZIP)
await client.users.deleteAccount(true);

Relay (中转站)

The relay is an OpenAI-compatible proxy gateway. Use it with the official OpenAI SDK - just swap the baseURL and apiKey (see below). The module covers key/channel/usage management.

// User side
const info = await client.relay.getInfo();          // relay base URL + available models
const key = await client.relay.createKey({ name: 'my-app' });
// key.key contains the od_ secret - save it now, it is shown only once!

// Admin side (super_admin)
const channels = await client.relay.listChannels();
await client.relay.createChannel({ name: 'OpenAI', base_url: 'https://api.openai.com/v1', api_key: 'sk-...' });
await client.relay.testChannel(channelId);

const usage = await client.relay.getUsage({ key_id: 1 });
const stats = await client.relay.getStats({ group_by: 'model' });
const trend = await client.relay.getTrend({ start: '2026-01-01' });
const routes = await client.relay.getModelRoutes();

Calling the relay with the official OpenAI SDK

import OpenAI from 'openai';

// 1. Get the relay base URL (or hardcode the server's relay URL)
const info = await client.relay.getInfo(); // { base_url, models }

// 2. Point the OpenAI SDK at the relay with an od_ key
const relay = new OpenAI({
  baseURL: info.base_url,          // e.g. https://relay.example.com/v1
  apiKey: 'od_xxx',                // relay key issued via client.relay.createKey()
});

const chat = await relay.chat.completions.create({
  model: info.models[0],
  messages: [{ role: 'user', content: 'Hello' }],
});

// Streaming works as usual:
const stream = await relay.chat.completions.create({
  model: info.models[0],
  messages: [{ role: 'user', content: 'Hi' }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}

Client Options

const client = new OpenDeskClient({
  baseUrl: 'https://your-server.com/api/v1', // API base URL
  token: 'sk_xxx',                           // optional: pre-set token / relay key
  headers: { 'X-Custom-Header': 'value' },   // optional: sent with every request

  timeoutMs: 30_000,     // default 30000 - request timeout
  retries: 0,            // default 0 - retries for idempotent requests (GET/DELETE), exponential backoff
  retryDelayMs: 500,     // default 500 - base backoff delay (multiplied by 2^n)

  fetch: customFetch,    // optional: inject a fetch implementation
  onRequest(method, path, init) {},   // optional: per-attempt request hook
  onResponse(method, path, response) {}, // optional: response hook
});

Timeout, Retry & Cancellation

// A timeout raises OpenDeskError with status 408.
// Retries apply only to idempotent methods (GET/DELETE) and never to
// user-cancelled requests; backoff is retryDelayMs * 2^n.

// Cancel a long-running request with AbortSignal:
const controller = new AbortController();
const data = await client.request('GET', '/skills/search', undefined, {
  params: { q: 'ai' },
  signal: controller.signal,
});
controller.abort(); // aborts in-flight requests

Downloading Binary Data

// Default: Blob (browser) / Buffer (Node)
const blob = await client.download('/users/me/export');

// Raw bytes:
const buf: ArrayBuffer = await client.download('/files/data.bin', undefined, 'arraybuffer');

Low-level requests

// Raw request with per-call overrides
const result = await client.request('POST', '/custom/endpoint', { payload: 1 }, {
  params: { page: 1 },
  headers: { 'X-Extra': 'yes' },
  responseType: 'json', // 'json' | 'blob' | 'arraybuffer'
  signal,
});

// Convenience methods: get / post / put / patch / delete / upload
await client.upload('/skills/upload', formData);

Pagination

// Method 1: Manual
const page1 = await client.skills.catalog({ page: 1, page_size: 20 });

// Method 2: Async generator (memory-efficient)
for await (const items of client.skills.catalogPaginator({}, 10)) {
  console.log('Batch:', items.length);
}

// Method 3: Fetch all
const all = await client.skills.catalogAll({}, 100);

Error Handling

Errors are typed by status code, and carry status, detail and an optional OpenAI-style code.

import {
  OpenDeskClient,
  AuthenticationError,     // 401
  PermissionDeniedError,   // 403
  NotFoundError,           // 404
  ValidationError,         // 422
  QuotaExceededError,      // 422 (quota message)
  ServerError,             // >= 500
} from '@opendesk/sdk';

try {
  await client.auth.login({ username: 'wrong', password: 'wrong' });
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('Login failed:', error.detail);
  } else if (error instanceof NotFoundError) {
    console.error('Not found:', error.detail);
  } else if (error instanceof OpenDeskError) {
    console.error(`API error [${error.status}] code=${error.code}:`, error.detail);
  }
}

Both FastAPI {detail: "..."} and OpenAI-style {"error": {"message": "...", "code": "..."}} error bodies are parsed automatically.

Environment Variables (SDK, Node.js only)

| Variable | Default | Description | |----------|---------|-------------| | OPENDESK_BASE_URL | http://127.0.0.1:8000/api/v1 | API base URL when baseUrl is not passed | | OPENDESK_TOKEN | - | Token used when token is not passed |

Requirements

  • Node.js >= 18 (for native fetch API)
  • Or modern browsers with fetch support

License

项目采用 Mulan PSL v2(Mulan Permissive Software License, Version 2,SPDX: MulanPSL-2.0)开源协议。