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

@kookee/sdk

v0.0.42

Published

Official Kookee SDK - Access your blog, changelog, help center, and more

Readme

@kookee/sdk

Official TypeScript SDK for Kookee - the headless CMS for your blog, changelog, and help center.

Features

  • Lightweight - Small minified footprint (ESM), no bloat
  • Zero dependencies - Uses native fetch, nothing else
  • TypeScript-first - Full type definitions out of the box
  • Tree-shakeable - Import only what you need
  • Open source - MIT licensed

Installation

npm install @kookee/sdk
# or
pnpm add @kookee/sdk
# or
yarn add @kookee/sdk

Quick Start

import { Kookee } from '@kookee/sdk';

const kookee = new Kookee({
  apiKey: 'your-api-key',
});

// Fetch blog posts
const posts = await kookee.blog.list({ limit: 10 });

// Get a single post by slug
const post = await kookee.blog.getBySlug('hello-world');

Configuration

const kookee = new Kookee({
  apiKey: 'your-api-key',
});

User Identification

Set user identity once and it's automatically used across all feedback operations:

// Set user identity globally
kookee.identify({
  externalId: 'user-456',
  name: 'Jane Doe',
  email: '[email protected]',   // optional
  avatarUrl: 'https://...',    // optional
});

// All feedback methods now auto-use the identified user
await kookee.feedback.createPost({ title: 'Feature request', category: 'feature' });
await kookee.feedback.createComment(postId, { content: 'Great idea!' });
const myPosts = await kookee.feedback.listMyPosts();

// Check current user
const user = kookee.getUser(); // KookeeUser | null

// Clear identity on logout
kookee.reset();

React usage

useEffect(() => {
  if (user) {
    kookee.identify({ externalId: user.id, name: user.name, email: user.email });
  } else {
    kookee.reset();
  }
}, [user]);

Blog

// List posts with pagination
const posts = await kookee.blog.list({ page: 1, limit: 10 });

// Filter by tag slugs
const taggedPosts = await kookee.blog.list({ tags: ['news'] });

// Search posts
const searchResults = await kookee.blog.list({ search: 'tutorial' });

// Get single post by slug
const post = await kookee.blog.getBySlug('my-post');

// Get single post by ID
const postById = await kookee.blog.getById('post-uuid');

// Get all tags with post counts
const tags = await kookee.blog.getTags();

// Get comments on a post
const comments = await kookee.blog.getComments('post-id', { page: 1, limit: 20 });

// React to a post
await kookee.blog.react('post-id', { reactionType: 'heart', action: 'add' });

// Get translations
const translationsBySlug = await kookee.blog.getTranslationsBySlug('my-post');
const translationsById = await kookee.blog.getTranslationsById('post-uuid');

Help Center

// List categories
const categories = await kookee.help.categories();

// List articles with pagination
const articles = await kookee.help.list({ page: 1, limit: 10 });

// Filter by category slug
const categoryArticles = await kookee.help.list({ category: 'getting-started' });

// Semantic search — results include a matched text snippet when available
const results = await kookee.help.search({ query: 'how to reset password', limit: 5 });
for (const result of results) {
  console.log(result.title, result.matchedChunk); // matched text snippet or null
}

// Get single article
const article = await kookee.help.getBySlug('getting-started');
const articleById = await kookee.help.getById('article-uuid');

// Get article translations
const translationsBySlug = await kookee.help.getTranslationsBySlug('getting-started');
const translationsById = await kookee.help.getTranslationsById('article-uuid');

// Get comments on an article
const comments = await kookee.help.getComments('article-id', { page: 1, limit: 20 });

// React to an article
await kookee.help.react('article-id', { reactionType: 'heart', action: 'add' });

// AI-powered chat
const response = await kookee.help.chat({
  messages: [{ role: 'user', content: 'How do I reset my password?' }],
  sessionId: 'optional-session-id', // maintain conversation context across calls
});

// Streaming chat
for await (const chunk of kookee.help.chatStream({ messages })) {
  if (chunk.type === 'delta') console.log(chunk.content);
  if (chunk.type === 'sources') console.log('Sources:', chunk.sources);
  if (chunk.type === 'done') console.log('Stream finished');
  if (chunk.type === 'error') console.error(chunk.message);
}

Changelog

// List entries
const entries = await kookee.changelog.list({ page: 1, limit: 10 });

// Search entries
const results = await kookee.changelog.list({ search: 'authentication' });

// Filter by typeSpecific properties (e.g. changelog type)
const features = await kookee.changelog.list({ filter: { changelogType: 'feature' } });

// Get single entry
const entry = await kookee.changelog.getBySlug('v1-0-0');
const entryById = await kookee.changelog.getById('entry-uuid');

// Get translations
const translationsBySlug = await kookee.changelog.getTranslationsBySlug('v1-0-0');
const translationsById = await kookee.changelog.getTranslationsById('entry-uuid');

// Get comments on an entry
const comments = await kookee.changelog.getComments('entry-id', { page: 1, limit: 20 });

// React to an entry
await kookee.changelog.react('entry-id', { reactionType: 'fire', action: 'add' });

Announcements

// List announcements
const announcements = await kookee.announcements.list({ page: 1, limit: 10 });

// Get single announcement
const announcement = await kookee.announcements.getById('announcement-uuid');

// Get translations
const translations = await kookee.announcements.getTranslationsById('announcement-uuid');

// Get comments on an announcement
const comments = await kookee.announcements.getComments('announcement-id', { page: 1, limit: 20 });

Pages

// List pages
const pages = await kookee.pages.list({ page: 1, limit: 10 });

// Search pages
const results = await kookee.pages.list({ search: 'privacy' });

// Get single page
const page = await kookee.pages.getBySlug('privacy-policy');
const pageById = await kookee.pages.getById('page-uuid');

// Get translations
const translationsBySlug = await kookee.pages.getTranslationsBySlug('privacy-policy');
const translationsById = await kookee.pages.getTranslationsById('page-uuid');

// Get comments on a page
const comments = await kookee.pages.getComments('page-id', { page: 1, limit: 20 });

Entries (Generic)

The entries module provides low-level access to all entry types through a unified API:

// List entries by type
const blogPosts = await kookee.entries.list({ type: 'blog', page: 1, limit: 10 });
const articles = await kookee.entries.list({ type: 'help_article', category: 'getting-started' });

// Filter by typeSpecific properties
const features = await kookee.entries.list({ type: 'changelog', filter: { changelogType: 'feature' } });

// Get entry by slug or ID
const entry = await kookee.entries.getBySlug('my-post', { type: 'blog' });
const entryById = await kookee.entries.getById('entry-uuid');

// Get translations
const translationsBySlug = await kookee.entries.getTranslationsBySlug('my-post');
const translationsById = await kookee.entries.getTranslationsById('entry-uuid');

// Get comments
const comments = await kookee.entries.getComments('entry-id', { page: 1, limit: 20 });

// React to any entry
await kookee.entries.react('entry-id', { reactionType: 'heart', action: 'add' });

// Get tags or categories for a type
const tags = await kookee.entries.getTags('blog');
const categories = await kookee.entries.getCategories('help_article');

Feedback

Reading feedback

// Get kanban columns (for roadmap rendering)
const columns = await kookee.feedback.getColumns();

// List feedback posts
const posts = await kookee.feedback.list({ page: 1, limit: 10 });

// Filter by column type: 'open' | 'closed'
const openPosts = await kookee.feedback.list({ columnType: 'open' });

// Filter by specific column
const planned = await kookee.feedback.list({ columnId: 'column-uuid' });

// Filter by category: 'feature' | 'improvement' | 'bug' | 'other'
const bugs = await kookee.feedback.list({ category: 'bug' });

// Sort options: 'newest' | 'top' | 'trending'
const trending = await kookee.feedback.list({ sort: 'trending' });

// Search posts
const results = await kookee.feedback.list({ search: 'dark mode' });

// Get single post with comments
const post = await kookee.feedback.getById('post-uuid');

// Vote on a post
await kookee.feedback.vote('post-id', { action: 'upvote' });

// Get top contributors
const contributors = await kookee.feedback.getTopContributors({ limit: 10 });

Creating and managing feedback

These operations require user identification — either globally via kookee.identify() or per-call via externalUser/externalId:

// With global identity (recommended — see User Identification section above)
kookee.identify({ externalId: 'user-123', name: 'Jane Doe' });

const newPost = await kookee.feedback.createPost({
  title: 'Add dark mode',
  description: 'It would be great to have a dark mode option.',
  category: 'feature',
});

const comment = await kookee.feedback.createComment('post-id', {
  content: 'Great idea, I would love this too!',
});

const myPosts = await kookee.feedback.listMyPosts();

await kookee.feedback.deletePost('post-id');
await kookee.feedback.deleteComment('comment-id');

Per-call override still works (takes precedence over global identity):

const newPost = await kookee.feedback.createPost({
  title: 'Add dark mode',
  externalUser: { externalId: 'other-user', name: 'John' },
});

const myPosts = await kookee.feedback.listMyPosts({ externalId: 'other-user' });
await kookee.feedback.deletePost('post-id', { externalId: 'other-user' });
await kookee.feedback.deleteComment('comment-id', { externalId: 'other-user' });

Config

// Get a single config value
const config = await kookee.config.getByKey('feature_flags');

// Get multiple config values
const configs = await kookee.config.list({ keys: ['feature_flags', 'theme'] });

Health Check

const health = await kookee.health();
// { status: 'ok', projectId: '...', timestamp: '...' }

Reactions

Blog posts, help articles, and changelog entries support reactions:

// Available reaction types: 'fire' | 'heart' | 'rocket' | 'eyes' | 'mindblown'
await kookee.blog.react('post-id', { reactionType: 'heart', action: 'add' });

// Remove a reaction
await kookee.blog.react('post-id', { reactionType: 'heart', action: 'remove' });

Localization

Most endpoints support locale options:

// Specify locale
const posts = await kookee.blog.list({ locale: 'de' });

// With fallback to default locale if translation doesn't exist
const post = await kookee.blog.getBySlug('hello-world', { locale: 'de', fallback: true });

Translation endpoints return a narrow EntryTranslationsMap keyed by locale code. Each value is a lightweight summary (id, slug, locale, title) — not a full entry. To load the full body of a translation, fetch it with getBySlug / getById using the target locale:

const translations = await kookee.blog.getTranslationsBySlug('hello-world');
// {
//   en: { id, slug, locale: 'en', title },
//   de: { id, slug, locale: 'de', title },
//   ...
// }

// To load the full German version:
const germanPost = await kookee.blog.getBySlug('hello-world', { locale: 'de' });

Paginated Response

All list endpoints return a paginated response:

interface PaginatedResponse<T> {
  data: T[];
  total: number;
  limit: number;
  page: number;
  totalPages: number;
}

List vs. Detail Responses

Entry endpoints come in two flavours with different shapes:

  • List responses (blog.list(), entries.list(), …) return *ListItem types — these do not include contentHtml. Use excerptHtml instead for previews.
  • Search responses (help.search()) return HelpSearchResult which extends the list item with matchedChunk: string | null — a plain-text snippet from the best matching section of the article.
  • Detail responses (blog.getBySlug(), help.getById(), …) return *Detail types — these include contentHtml for full content rendering.
// List: no contentHtml, only excerptHtml
const posts = await kookee.blog.list({ limit: 10 });
for (const post of posts.data) {
  renderPreview(post.excerptHtml); // ✅ available on list
  // renderFull(post.contentHtml);  // ❌ type error — not on list
}

// Detail: contentHtml is available
const post = await kookee.blog.getBySlug('hello-world');
renderFull(post.contentHtml); // ✅ available on detail

Categories on entries

Every entry response (list and detail) includes both categoryId: string | null and a resolved category: EntryCategoryRef | null. No client-side join required:

const results = await kookee.help.search({ query: 'how to reset password' });
for (const result of results) {
  // result.category is already populated by the server
  console.log(result.title, '→', result.category?.name);
}

type EntryCategoryRef = {
  id: string;
  slug: string;
  name: string;
  icon: string | null;
  description: string | null;
};

When an entry has no category assigned, category is null and categoryId is null — guard accordingly.

Error Handling

import { Kookee, KookeeApiError } from '@kookee/sdk';

try {
  const post = await kookee.blog.getBySlug('non-existent');
} catch (error) {
  if (error instanceof KookeeApiError) {
    console.error(`Error ${error.code}: ${error.message}`);
    console.error(`Status: ${error.status}`);
  }
}

Code Block Styles

The SDK ships an optional CSS file for styling code blocks in content HTML (VS Code Dark+ theme):

import '@kookee/sdk/styles/code.css';

Or via CDN:

<link rel="stylesheet" href="https://unpkg.com/@kookee/sdk/styles/code.css">

This provides:

  • Syntax-highlighted code blocks with a dark theme
  • Copy-to-clipboard button styling
  • Language label display
  • Inline code styling (red on pink background)

TypeScript

The SDK is written in TypeScript and provides full type definitions:

import type {
  // Entry base & shared
  BaseEntry,
  EntryDetailFields,
  EntryType,
  EntryStatus,
  EntryAuthor,
  EntryTag,
  EntryTagWithCount,
  EntryCategory,
  EntryCategoryRef,
  EntryComment,
  EntryTranslationSummary,
  EntryTranslationsMap,

  // Entry variants — LIST responses (no contentHtml)
  GenericEntryListItem,
  BlogEntryListItem,
  PageEntryListItem,
  HelpArticleListItem,
  ChangelogEntryListItem,
  AnnouncementListItem,
  TypedEntryListItem,
  AnyEntryListItem,

  // Entry variants — DETAIL responses (with contentHtml)
  GenericEntryDetail,
  BlogEntryDetail,
  PageEntryDetail,
  HelpArticleDetail,
  ChangelogEntryDetail,
  AnnouncementDetail,
  TypedEntryDetail,
  AnyEntryDetail,

  // Type-specific metadata
  TypeSpecific,
  BlogTypeSpecific,
  PageTypeSpecific,
  HelpArticleTypeSpecific,
  ChangelogTypeSpecific,
  AnnouncementTypeSpecific,

  // Changelog & Announcement specific
  ChangelogType,
  AnnouncementType,

  // Help Center
  HelpSearchResult,
  HelpArticleVisibility,
  HelpChatMessage,
  HelpChatParams,
  HelpChatResponse,
  HelpChatSource,
  HelpChatSourceCategory,
  HelpChatStreamChunk,

  // Feedback
  FeedbackKanbanColumn,
  FeedbackPost,
  FeedbackPostListItem,
  FeedbackColumnType,
  FeedbackPostCategory,
  FeedbackSortOption,
  FeedbackAuthor,
  FeedbackAssignee,
  FeedbackComment,
  FeedbackTopContributor,
  FeedbackVoteResponse,
  CreateFeedbackPostParams,
  CreateFeedbackCommentParams,
  CreatedFeedbackPost,
  CreatedFeedbackComment,
  ListMyFeedbackPostsParams,
  DeleteFeedbackPostParams,
  DeleteFeedbackPostResponse,
  DeleteFeedbackCommentParams,
  DeleteFeedbackCommentResponse,

  // User & Identity
  KookeeUser,
  ExternalUser,

  // Reactions
  ReactionType,
  ReactParams,
  ReactResponse,

  // Common
  ApiError,
  PublicConfig,
  PaginatedResponse,
  PaginationParams,
  LocaleOptions,
  OrderDirection,
  KookeeConfig,
  HealthCheckResponse,

  // Module request parameter shapes
  EntriesListParams,
  EntriesGetByIdParams,
  EntriesGetBySlugParams,
  EntriesGetCommentsParams,
  EntriesGetCategoriesParams,
  BlogListParams,
  BlogGetBySlugParams,
  BlogGetByIdParams,
  BlogGetCommentsParams,
  HelpCategoriesParams,
  HelpListParams,
  HelpSearchParams,
  HelpGetBySlugParams,
  HelpGetByIdParams,
  HelpGetCommentsParams,
  ChangelogListParams,
  ChangelogGetBySlugParams,
  ChangelogGetByIdParams,
  ChangelogGetCommentsParams,
  PagesListParams,
  PagesGetBySlugParams,
  PagesGetByIdParams,
  PagesGetCommentsParams,
  AnnouncementListParams,
  AnnouncementGetByIdParams,
  AnnouncementGetCommentsParams,
  ConfigListParams,
  FeedbackListParams,
  FeedbackVoteParams,
  FeedbackTopContributorsParams,
} from '@kookee/sdk';

License

MIT