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

@cogability/sdk

v0.9.2

Published

Framework-agnostic JavaScript SDK for CogAbility CAM and CMG APIs

Readme

@cogability/sdk

Framework-agnostic JavaScript SDK for the CogAbility platform.

Works in browser (React, Vue, vanilla JS, Lovable) and Node.js (agents, servers, CI scripts).

Installation

npm install @cogability/sdk

For browser OIDC flows (login/logout), also install the peer dependency:

npm install oidc-client-ts

Three clients

| Client | Purpose | Works in | |---|---|---| | CamClient | Chat sessions, streaming messages | Browser + Node.js | | CmgClient | Membership validation, geofencing | Browser + Node.js | | AuthClient | OIDC login / callback via App ID | Browser only |


CamClient — anonymous chat (browser SPA)

import { CamClient, BrowserSessionStore } from '@cogability/sdk';

const cam = new CamClient({
  host: 'https://cam.example.com',  // omit in Vite dev (uses proxy)
  cogbotId: 'mc_0091:full',
  sessionStore: new BrowserSessionStore(),
});

// 1. Establish session + fetch config
await cam.initAnonymous();
const config = await cam.initCogbot();
const greeting = await cam.fetchGreeting();

// 2. Send a message (non-streaming)
const response = await cam.sendMessage('Hello');
const items = CamClient.parseResponseGeneric(response);
console.log(items[0].text);

// 3. Send a message (streaming)
for await (const { eventName, data } of cam.streamMessage('Tell me more')) {
  if (eventName === 'partial_object') {
    process.stdout.write(CamClient.parseResponseGeneric(data)[0]?.text ?? '');
  } else if (eventName === 'final_response') {
    console.log('\n[done]');
  }
}

CamClient — authenticated chat (browser SPA)

After the user logs in and you have their idToken:

await cam.initAuthenticated(idToken);
for await (const event of cam.streamMessage('Save my profile', { anonymous: false })) {
  // handle event
}

Expired sessions

CAM sessions live on the server and expire on their own schedule, so a client that has been idle can still be holding a valid idToken and be refused. When a request comes back 401, the SDK re-runs whichever init established the session and retries the request once. Anonymous sessions restore silently; the user sees nothing.

Authenticated sessions need a live idToken, and by the time the session has lapsed the token the app passed to initAuthenticated has often lapsed with it. Supply getIdToken so the SDK can fetch a current one instead of replaying a stale one. It is called at recovery time, so read from live storage rather than closing over a value:

const cam = new CamClient({
  cogbotId: 'mc_0091:full',
  getIdToken: () => sessionStorage.getItem('cam_token'),
});

Without getIdToken the SDK falls back to the token given to initAuthenticated, which works only while that token is still valid.

When the session cannot be restored without a login, the SDK throws CamSessionExpiredError rather than a bare 401. Treat it as "ask the user to sign in again" — a plain retry cannot succeed:

import { CamSessionExpiredError } from '@cogability/sdk';

try {
  await cam.sendMessage(text);
} catch (err) {
  if (err instanceof CamSessionExpiredError) promptSignIn();
  else showGenericError();
}

Two limits are deliberate. A request is retried at most once, so a second 401 surfaces instead of looping. And streamMessage only retries while opening the stream — once events have been yielded, replaying would emit them twice, so a mid-stream failure is reported rather than recovered.

CmgClient — membership validation (any framework)

import { CmgClient } from '@cogability/sdk';

const cmg = new CmgClient({
  host: 'https://cmg.example.com',
  namespace: 'my-namespace',
});

// Validate a logged-in user's membership
const { isMember, roles, geofenced, codeRequired } = await cmg.validateMembership(idToken);

// Check geofence for an anonymous visitor (fails open)
const { geofenced, message } = await cmg.checkGeofence();

validateMembership(idToken, namespace?) — return shape (MembershipResult)

| Field | Type | Description | |---|---|---| | isMember | boolean | CMG confirmed namespace membership. | | autoProvisioned | boolean | CMG auto-created the membership record on this login. | | hasProfile | boolean | Member has a stored profile in CMG. | | roles | Role[] | Roles granted to this user in the namespace. | | geofenced | boolean | This IP is outside the allowed region. | | geofenceMessage | string | null | Human-readable message when geofenced. | | codeRequired | boolean | The namespace requires an access code to join. true when the user is authenticated but not yet a member because the namespace is code-gated. See redeemCode below. |

redeemCode({ idToken, code, namespace? }) — access-code redemption

Provisions membership for a code-gated namespace. Call this after validateMembership returns codeRequired: true and the user has entered a code.

On success the server auto-provisions the user and returns resolved roles (same shape as a successful validateMembership). On an invalid or expired code the server returns HTTP 400 and the result is returned (not thrown) so the caller can surface a retry UI. On HTTP 503 an error is thrown.

| Option | Type | Required | Description | |---|---|---|---| | idToken | string | yes | App ID JWT id_token. | | code | string | yes | Access code entered by the user. | | namespace | string | no | Override the namespace set at construction. |

Returns RedeemCodeResult:

| Field | Type | Description | |---|---|---| | isMember | boolean | true when the code was valid and membership was provisioned. | | autoProvisioned | boolean | true when CMG auto-created the membership record. | | roles | Role[] | Roles granted on success; empty on failure. | | geofenced | boolean | IP is outside the allowed region. | | geofenceMessage | string | null | Human-readable geofence message. | | codeRequired | boolean | true when the code was rejected and a retry is still possible. | | error | string | null | 'invalid_code' on a bad/expired code, 'wrong_product' when the code is genuine but belongs to a different namespace's product (not consumed — retryable on the site it was issued for), null on success. | | message | string | null | Human-readable explanation. Only ever set alongside error: 'wrong_product' — every other failure omits it on purpose so the API's one generic "wrong code, expired, disabled, already used, etc." message can't be picked apart from the client side. Safe to show verbatim when present. |

Error behavior:

| HTTP status | Behavior | |---|---| | 200 (success) | Returns result with isMember: true, roles populated. | | 400, wrong product | Returns result with isMember: false, error: 'wrong_product', codeRequired: true, and a message worth showing. The code was not consumed. | | 400, any other reason | Returns result with isMember: false, error: 'invalid_code', codeRequired: true, message: null. | | 503 (service unavailable) | Throws an Error. |

// After validateMembership returns codeRequired: true
const result = await cmg.redeemCode({ idToken, code: 'ABC-123' });

if (result.isMember) {
  console.log('Access granted. Roles:', result.roles);
} else if (result.error === 'wrong_product') {
  console.warn(result.message); // e.g. "That access code is for a different CogBot..."
} else if (result.error === 'invalid_code') {
  console.warn('Invalid or expired code — prompt the user to retry');
} else if (result.geofenced) {
  console.warn('Geofenced:', result.geofenceMessage);
}

AuthClient — OIDC login flow (browser only)

import { AuthClient } from '@cogability/sdk';

const auth = new AuthClient({
  authorityUrl: 'https://us-south.appid.cloud.ibm.com/oauth/v4/YOUR_TENANT',
  clientId: 'YOUR_CLIENT_ID',
  redirectUri: `${window.location.origin}/callback`,
  // Routes the token exchange through CMG: App ID's endpoint disallows browser
  // CORS, and CMG attaches the confidential client's secret server-side. Required.
  tokenEndpointProxy: 'https://cmg.example.com/auth/token',
});

// Trigger login redirect
await auth.login('/members');

// On the /callback page:
const { user, idToken } = await auth.handleCallback();
console.log('Logged in as', user.email);

// Later:
await auth.logout();

Signing in with tokens from an external broker

signInWithExternalTokens() adopts App ID tokens that were obtained somewhere other than this client and stores them as the current session. It exists for the Microsoft sign-in broker, which runs the Microsoft OIDC flow server-side and exchanges the result for real App ID tokens — the browser never sees an authorization code, so there is nothing for handleCallback() to process.

// The broker redirected back with ?ms_auth_code=... — trade it for tokens.
const res = await fetch('https://idbroker.example.net/auth/microsoft/redeem', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code }),
});
const tokens = await res.json();

const { user } = await auth.signInWithExternalTokens(tokens);

Afterwards the session behaves like any other: getUser(), getIdToken(), logout(), and the API clients all work unchanged.

Two caveats:

  • No userLoaded event is raised, because oidc-client-ts does not expose a way to raise one. Use the returned value, as you would with handleCallback().
  • Brokered sessions do not carry a refresh token, so they last one access-token lifetime rather than being silently renewed. App ID binds a refresh token to the client that obtained it, and a broker is a confidential client, so the CMG token proxy could not renew one anyway.

Node.js agent — programmatic CogBot access

Agents skip OIDC entirely and pass tokens directly.

import { CamClient, CmgClient, MemorySessionStore } from '@cogability/sdk';

const cam = new CamClient({
  host: 'https://cam.example.com',
  cogbotId: 'mc_0091:full',
  sessionStore: new MemorySessionStore(),
  getHostUrl: () => 'https://agent.example.com',
});

const cmg = new CmgClient({
  host: 'https://cmg.example.com',
  namespace: 'my-namespace',
});

// Anonymous session — no login required
await cam.initAnonymous();
await cam.initCogbot();

// Check membership status for a known user token
const membership = await cmg.validateMembership(agentIdToken);
if (membership.isMember) {
  await cam.initAuthenticated(agentIdToken);
}

// Stream a conversation
for await (const { eventName, data } of cam.streamMessage('What are my membership benefits?')) {
  if (eventName === 'final_response') {
    const text = CamClient.parseResponseGeneric(data)
      .filter(g => g.response_type === 'text')
      .map(g => g.text)
      .join('\n');
    console.log(text);
  }
}

Vue / vanilla JS — drop-in chat widget

import { CamClient, BrowserSessionStore } from '@cogability/sdk';

const cam = new CamClient({
  host: import.meta.env.VITE_COGBOT_HOST,
  cogbotId: import.meta.env.VITE_COGBOT_ID,
  sessionStore: new BrowserSessionStore(),
});

await cam.initAnonymous();
await cam.initCogbot();

document.getElementById('send').addEventListener('click', async () => {
  const text = document.getElementById('input').value;
  const output = document.getElementById('output');
  output.textContent = '';

  for await (const { eventName, data } of cam.streamMessage(text)) {
    if (eventName === 'partial_object') {
      const parts = CamClient.parseResponseGeneric(data)
        .filter(g => g.response_type === 'text');
      if (parts[0]) output.textContent = parts[0].text;
    }
  }
});

Chat sessions and conversation history

Starting a new chat

CamClient automatically mints and persists a chat_id UUID on first use. The same chat_id is included in every _buildMessageBody() call so the PFC2 backend can key the RAG conversation to a single LangGraph checkpoint (general_thread_id).

When the user clicks "New Chat", call rotateChatId() before re-initializing:

// New Chat — creates a fresh RAG checkpoint on the next message turn
cam.rotateChatId();
await cam.initAnonymous(); // or initAuthenticated(idToken)
await cam.initCogbot();
const greeting = await cam.fetchGreeting();

rotateChatId() mints a new UUID, persists it in the session store under buddy_chat_id, and returns it. All subsequent sendMessage / streamMessage calls will carry the new chat_id, and PFC2 will start a fresh conversation thread with no prior history.

Fetching conversation history

// Retrieve the current DI + RAG conversation thread
const history = await cam.fetchConversationHistory();

// Or retrieve any past conversation by its chat_id (added in 0.4.0)
const past = await cam.fetchConversationHistory('chat-uuid-from-listConversations');

console.log(history.turns);
// [
//   { role: 'user', content: 'What can you do?' },
//   { role: 'assistant', content: 'I can answer your questions.' },
// ]

console.log(history.transcript_text);
// "User: What can you do?\nAssistant: I can answer your questions."

if (history.summary) {
  console.log('Summarized prefix:', history.summary);
}

fetchConversationHistory(chatId?) calls GET /api/cogbots/{cogbotId}/id/{uid}/conversation-history?chat_id=... on the PFC2 backend. When chatId is omitted the SDK falls back to the current buddy_chat_id in the session store. Pass an explicit id (e.g. one returned by listConversations()) to load a different past conversation. The response shape is:

| Field | Type | Description | |---|---|---| | thread_id | string | The LangGraph general_thread_id for this chat | | chat_id | string | The chat_id that was sent with messages | | turns | { role, content }[] | Human/assistant exchanges in order; SDI turns excluded | | transcript_text | string | Plain-text transcript, suitable for live-agent handoff | | summary | string | null | Rolling summary from SummarizationMiddleware if triggered |

Listing a member's prior conversations

Available in @cogability/[email protected]+. Authenticated sessions only.

When the user signs in via initAuthenticated(idToken), the SDK can enumerate every past chat thread that member has on record — useful for rendering a "Previous Chats" sidebar (a la babybrain.ai or ChatGPT's left rail).

await cam.initAuthenticated(idToken);
const { conversations } = await cam.listConversations();

// [
//   {
//     chat_id: '7e5b...',
//     last_updated: '2026-05-26T20:00:00Z',
//     title: 'When should my baby start solids?',
//     turn_count: 4,
//   },
//   ...
// ]

// Click handler: load a prior conversation into the chat widget
async function openPastChat(chatId) {
  const { turns } = await cam.fetchConversationHistory(chatId);
  renderTurns(turns);
}

listConversations() calls GET /api/cogbots/{cogbotId}/id/{uid}/conversations on the PFC2 backend. Response shape:

| Field | Type | Description | |---|---|---| | conversations[].chat_id | string | UI chat identifier; pass to fetchConversationHistory(chatId) | | conversations[].last_updated | string | ISO 8601 timestamp of the latest checkpoint in this thread | | conversations[].title | string | null | First user message, ellipsized to 80 chars; null if the thread has no human turn | | conversations[].turn_count | number | null | Count of human + assistant exchanges, excluding rolling summary system messages |

Conversations are returned newest-first, capped at 50 by the backend. Anonymous sessions receive an empty list (the underlying uid is a per-browser random UUID with no server-side enumeration). The endpoint requires an authenticated CAM session — see be-pfc/docs/cascade-architecture.md — Conversation List API for backend details.

Session storage keys

| Key | Description | |---|---| | buddy_user_id | Stable user UID minted on first anonymous session | | buddy_cogbot_sid | Safari-compatible session cookie fallback | | buddy_chat_id | Current chat UUID; rotated by rotateChatId() |

Fetching the profile schema (driven by the cogbot's major)

const schema = await cam.fetchProfileSchema();

if (schema) {
  // Render a dynamic form from schema.sections — e.g. with the
  // <DynamicProfileForm> component in @cogability/membership-kit.
} else {
  // No profile_schema configured on this cogbot's major yet.
  // Render the app's built-in (hard-coded) form.
}

fetchProfileSchema() calls GET /api/cogbots/{cogbotId}/profile-schema on the PFC2 backend. The server resolves cogbot_id -> CogBotConfig.major_name -> CogMajorConfig.profile_schema and returns the typed schema. Returns null on HTTP 404 so callers can fall back to a built-in form during rollout — the schema is published doc-by-doc on each major, not all at once.

Response shape (all fields optional, snake_case on the wire):

| Field | Type | Description | |---|---|---| | version | number | Schema version. Currently 1. | | sections | ProfileSection[] | Ordered list of form sections (e.g. parent, children). | | extras_bucket | ProfileExtrasBucket | Optional bucket for ad-hoc keys not covered by any section. |

Each ProfileSection has a section_type of "object" (single record, e.g. parent) or "list" (repeating items, e.g. children). Each ProfileField has a field_type of text | textarea | date | number | select | multiselect | boolean, plus optional required, options, min_value / max_value, pattern, and a small show_when expression for conditional visibility (e.g. "birthContext == 'premature'"). See types.js for the full JSDoc typedefs.

No session or chat_id is required for this endpoint; the schema is per-cogbot, non-sensitive UI metadata. Auth is the same JWT/Basic surface as the rest of /api/cogbots/*.


Session storage

| Class | When to use | |---|---| | PersistentBrowserSessionStore | Browser SPAs with an anonymous turn limit — wraps window.localStorage | | BrowserSessionStore | Browser SPAs where the session should end with the tab — wraps window.sessionStorage | | MemorySessionStore | Node.js, SSR, tests — in-process Map |

Custom stores implement three methods: get(key), set(key, value), remove(key).

Choosing between the two browser stores

BrowserSessionStore is scoped to a single tab, so closing the tab hands the visitor a brand new uid — and with it, a fresh turn allowance. If your site meters anonymous chat, prefer PersistentBrowserSessionStore, which keeps the uid in localStorage so it survives tab closes and browser restarts:

import { CamClient, PersistentBrowserSessionStore } from '@cogability/sdk';

const cam = new CamClient({
  host: 'https://cam.example.com',
  cogbotId: 'mc_0091:full',
  sessionStore: new PersistentBrowserSessionStore(),
});

Switching an existing site over is safe: reads migrate any value already in sessionStorage into localStorage, so visitors mid-session are not reset. The store also degrades rather than throwing when storage is unreachable — during server-side rendering, in Safari private mode, or when a write hits a quota error — falling back to tab-scoped storage so the visit still works.

Advanced: raw SSE parsing

import { parseSseBlock, parseSseStream } from '@cogability/sdk';

// Parse a single SSE block string
const event = parseSseBlock('event: partial_object\ndata: {"output":{"generic":[]}}');

// Parse a fetch Response body as a stream
const res = await fetch('https://cam.example.com/api/cogbots/my-bot/id/uid/message/stream', {
  method: 'POST', body: JSON.stringify(payload), credentials: 'include',
});
for await (const event of parseSseStream(res)) {
  console.log(event.eventName, event.data);
}

Environment variables (Vite)

When using inside the membership-kit or template, the SDK reads these via import.meta.env:

| Variable | Used by | |---|---| | VITE_COGBOT_HOST | CamClient host (production) | | VITE_COGBOT_ID | CamClient cogbotId | | VITE_CMG_URL | CmgClient host | | VITE_SITE_NAMESPACE | CmgClient namespace | | VITE_APPID_OAUTH_SERVER_URL | AuthClient authorityUrl | | VITE_APPID_CLIENT_ID | AuthClient clientId |