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

@tstax/coding-tab

v0.2.8

Published

Drop-in Cursor-style coding agent tab for any Express app. Multiple persistent chats, plan/agent modes, in-line tool inspection, GitHub OAuth, opens PRs against your repo via the Cursor SDK cloud runtime.

Downloads

144

Readme

@tstax/coding-tab

Drop a Cursor-style coding agent tab into any Express app. Sign in with GitHub, run multiple persistent chats in Plan or Agent mode, inspect every tool call inline, then Merge & Redeploy to land a PR — and let your existing GitHub-to-Railway (or Vercel, or Render) auto-deploy ship the change.

Powered by @cursor/sdk cloud runtime, so the agent behaves the same as Cursor's own chat: real plan/agent loops, real file editing, real PRs.

What's new in 0.2

  • Multiple chats at once. A left sidebar lists all your chats, each with its own conversation context. Switch between them at any time, even while one is streaming.
  • Full persistence. Chat metadata, every turn, and every tool call (with args + result) is stored on disk. Refresh the tab and your history is right where you left it. Plug in your own database via the storage option.
  • Plan rendered as preview. Plan-mode answers render as proper markdown — headings, lists, code blocks, links — instead of raw text.
  • Chronological timeline. Agent text and tool invocations are interleaved in the order they actually happened, with a clear paragraph break between each thought.
  • Click into any tool call. Every grep_search, read_file, task, shell, etc. is a collapsible row showing the full args and the full result.

Patch releases

0.2.8 — fixes a regression from 0.2.7's "preserve draft across re-renders" change: after clicking Send, the textarea would keep showing the just-sent message. The defensive captureComposerDraft() at the top of render() was re-reading the not-yet-replaced DOM textarea and resurrecting the draft that onSend had just cleared. The textarea's input listener already mirrors every keystroke into state, so the in-render capture was redundant; removing it fixes the clear without losing the "draft survives mode/model/chat switch" behavior.

0.2.7 — four mobile/composer UX upgrades:

  • Photo & file attachments. A paperclip button in the composer opens the OS file picker; pick one or more images and they show up as removable thumbnails above the textarea. On send, each image is downscaled to 1600px on its longest edge via <canvas> (so a 4000px camera photo doesn't blow out the SSE pipe), base64-encoded, and forwarded straight to agent.send({ text, images }) as an SDKUserMessage. The images are persisted on the user turn and re-render as click-to-zoom thumbnails when you scroll back. Up to 8 images per message; PNG screenshots stay lossless, JPEGs recompress at q=0.85. The server's body limit defaults to 25mb (configurable via the new bodyLimit mount option).
  • Composer draft now persists across plan/agent toggle, model dropdown change, chat switch, and every render triggered by streaming events. Each chat keeps its own draft + pending attachments, so jumping between threads doesn't make you retype anything. A small __pending__ slot also catches text the user typed before clicking "+ New chat" so the first message of a new chat survives the chat-create roundtrip.
  • Tap-outside-to-close sidebar on mobile. When the slide-over chat list is open, the area to the right is now a dimmed scrim — tap it once and the sidebar closes, matching every native iOS/Android drawer.
  • More breathing room at the bottom. The composer's bottom padding now uses max(14px, ...) instead of a flat 10px, so even on hosts that don't expose env(safe-area-inset-bottom) the Send button isn't flush against the rounded screen edge. The mobile sidebar's chat list also respects the safe-area inset so the last chat row doesn't sit under the iOS home indicator.

0.2.6 — fixes two visible bugs:

  • Plan-mode no longer renders as a stream of fragments. The Cursor SDK streams text as small incremental deltas (each assistant message is a chunk like "I have", " en", "ough"); previous releases ended the text block after every push, persisting each delta as its own paragraph. 0.2.6 only ends the block when a tool call interrupts, so consecutive deltas coalesce. The client also coalesces at render time so chats persisted before the fix display correctly too.
  • Mobile composer no longer cuts off the buttons. On screens ≤720px the textarea now takes the full width (so the placeholder doesn't wrap into a tiny strip) and Send/Stop stack beneath it with a 40px tap target. Padding-bottom uses env(safe-area-inset-bottom) so the iOS home indicator doesn't crop the buttons. The standalone /coding-tab/ host page also gets explicit html, body { height: 100dvh }.

0.2.5 — handles two more mobile-backgrounding failure modes that 0.2.4 missed:

  • Upstream stream disconnects (NetworkError, "Load failed", "fetch failed", "ECONNRESET", etc.) are no longer treated as fatal turn errors. The turn's status is preserved, no [error] Load failed text gets persisted into the timeline, and a background run.wait() reattach with backoff updates the terminal status (and any PR/branch info) once the cloud run finishes.
  • AgentBusyError (409) on a follow-up send is surfaced as a friendly "the agent is still working on the previous request…" message instead of [error] [agent_busy] Agent already has an active run. The empty assistant turn that would otherwise become noise is marked cancelled. If the chat ever stays wedged because Cursor's cloud is reporting a phantom active run, the user can recover by deleting the chat (trash icon) and starting a new one — that creates a fresh agent.

0.2.4 — auto-recover when the cached cloud agent has been GC'd: agent.send() failures with agent_not_found transparently drop the stale in-memory handle, clear the persisted agentId, and create a fresh agent on retry instead of surfacing [error] Agent not found to the user.

0.2.3 — survive mobile Safari backgrounding by polling instead of erroring.

0.2.2 — Agent-mode link rendering + GitHub fallback when PR creation is blocked by repo/team policy.

0.2.1 — mobile UX fixes + Merge & Redeploy button feedback (loading / success / error).

Install

npm install @tstax/coding-tab

What you need

  1. Cursor API key — from https://cursor.com/dashboard/cloud-agents.
  2. A GitHub OAuth App for each app that embeds this tab (one-time, ~2 minutes per app):
  3. Node 20+ running an Express server.
  4. A writable directory for the default file store, OR your own ChatStorage adapter (see Persistence).

Mount it on your server

import express from "express";
import { mountCodingTab } from "@tstax/coding-tab/server";

const app = express();

mountCodingTab(app, {
  cursorApiKey: process.env.CURSOR_API_KEY!,
  githubOAuth: {
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    callbackUrl: `${process.env.PUBLIC_BASE_URL}/coding-tab/auth/callback`,
    allowedLogins: (process.env.ALLOWED_GITHUB_LOGIN ?? "").split(",").filter(Boolean),
  },
  sessionPassword: process.env.SESSION_SECRET!, // 32+ chars: openssl rand -hex 32
  defaultRepo: { url: "https://github.com/youruser/yourrepo" },
  // Optional — defaults to `<cwd>/.coding-tab-data`
  dataDir: process.env.CODING_TAB_DATA_DIR,
});

app.listen(3000);

That's it on the server. Visit https://<your-app>/coding-tab/ and the tab is fully self-rendering. Or embed it inside your dashboard:

<div id="ct"></div>
<link rel="stylesheet" href="/coding-tab/style.css" />
<script src="/coding-tab/browser.js"></script>
<script>
  CodingTab.mountCodingTab(document.getElementById("ct"), { apiBase: "/coding-tab" });
</script>

Required env vars

| Var | What it is | | --- | --- | | CURSOR_API_KEY | API key from cursor.com/dashboard/cloud-agents | | GITHUB_CLIENT_ID | From your OAuth App | | GITHUB_CLIENT_SECRET | From your OAuth App | | SESSION_SECRET | 32+ char random string (openssl rand -hex 32) | | ALLOWED_GITHUB_LOGIN | Comma-separated GitHub usernames allowed to sign in | | PUBLIC_BASE_URL | Your app's external URL, used to build the OAuth callback |

Attachments

The composer's paperclip button accepts any image/* file. Selected photos are:

  1. Read into memory via FileReader.readAsDataURL.
  2. Decoded into an <img> and re-encoded through a <canvas> sized to at most 1600px on the longest edge (aspect ratio preserved). PNGs round-trip as PNG; everything else recompresses to JPEG at q=0.85. This keeps payloads in the low hundreds of KB for typical phone photos.
  3. Sent to /agent/send as { images: [{ mimeType, data }] } where data is base64 with no data: prefix.
  4. Forwarded to the SDK as an SDKUserMessage so the model can see them.
  5. Persisted on the user turn under images: AttachedImage[] so they re-render when the chat is loaded back.

Limits, all enforced server-side:

  • Max 8 images per message (extras are silently dropped).
  • Each image is at most the chat's body limit's share — bump bodyLimit on mountCodingTab(...) if your users routinely attach many large screenshots (default 25mb).

Persistence

Each chat is keyed to a GitHub login and contains a chronological list of turns; each assistant turn is a chronological list of TimelineEvents (text paragraphs and tool invocations).

By default, this is stored as JSON files under dataDir (<cwd>/.coding-tab-data if you don't set it):

<dataDir>/
  chats/<chatId>.json    # full chat: metadata + every turn + every tool call
  index/<login>.json     # cached listing per user (rebuilt on demand if missing)

Pluggable storage

The default file store is fine for a single-process app on a host with a real (or mounted) filesystem. For ephemeral hosts (Railway free tier, Vercel, etc.) or multi-instance deployments, plug in your own:

import type { ChatStorage } from "@tstax/coding-tab/server";

const supabaseStorage: ChatStorage = {
  async listChats(login) { /* SELECT id, title, ... FROM chats WHERE login = $1 */ },
  async loadChat(id, login) { /* … */ },
  async createChat(chat) { /* … */ },
  async patchChat(id, login, patch) { /* … */ },
  async appendTurn(turn) { /* … */ },
  async patchTurn(chatId, turnId, patch) { /* … */ },
  async deleteChat(id, login) { /* … */ },
};

mountCodingTab(app, { ...config, storage: supabaseStorage });

Resuming agents across restarts

Each chat persists the agentId of the cloud agent it last spoke to. When the user sends another message in that chat, the server calls Agent.resume(agentId) so the model picks up the same conversation. If the cloud agent has been garbage-collected, we fall back to Agent.create(); the persisted turn history still displays.

Repo selection

The repo the agent works against is whatever you pass in defaultRepo.url server-side. The client widget reads this from /auth/me and locks the URL field to that repo (rendered as a clickable org/repo pill in the header), so users can't accidentally point the agent at a different codebase. To change the repo, change the server config — there's intentionally no UI override.

Security model

  • GitHub OAuth, single sign-in. The user signs in with GitHub once. The OAuth access token authenticates the session AND authorizes clone/push/PR/merge — no separate PAT to manage.
  • Allowlist. Set allowedLogins to your GitHub username (and anyone else you trust). Anyone outside the list gets a 403 even with a valid GitHub login.
  • Per-user chat scoping. Every persistence call is scoped to the signed-in user's GitHub login; one user can never read or mutate another user's chats.
  • HTTP-only signed encrypted cookie. Session is stored in an iron-session cookie (coding_tab_session): JS on the page can't read the access token. Server decrypts on every request.
  • CSRF-protected handshake. OAuth uses a per-request state cookie that expires in 10 minutes.
  • Cookie hardening. HttpOnly, SameSite=Lax, Secure in production.

Plan vs Agent mode

  • Plan — agent runs read-only, produces a markdown plan ending in PLAN READY. The plan renders as a proper preview (headings, lists, code, links). Click Execute plan to follow up with code changes in the same conversation.
  • Agent — agent goes straight to making changes and opens a PR (autoCreatePR: true).

The conversation context is preserved across plan and execute turns, just like Cursor chat.

Models

The picker shows Sonnet and Opus, mapped to whatever Claude variants your Cursor account exposes via Cursor.models.list(). If your account stops offering Sonnet/Opus directly (Cursor occasionally changes what's available), the dropdown will only show what's accessible.

API surface (mounted at basePath, default /coding-tab)

Public:

  • GET / — host HTML
  • GET /style.css, GET /browser.js — assets
  • GET /auth/login — start OAuth
  • GET /auth/callback — OAuth return
  • POST /auth/logout
  • GET /auth/me — 401 if not signed in, otherwise { githubLogin, avatarUrl?, defaultRepoUrl?, defaultRepoRef? }

Authenticated (require valid session):

  • GET /chats — list current user's chats
  • POST /chats — create a new chat (returns { chat })
  • GET /chats/:id — full chat including all turns
  • PATCH /chats/:id — rename / change mode or model
  • DELETE /chats/:id — delete chat + dispose any in-memory agent
  • GET /models — discovered Sonnet/Opus IDs
  • POST /agent/start — alias of /agent/send
  • POST /agent/send — SSE stream; sends a turn into the given chat (creates or resumes the cloud agent)
  • POST /agent/execute — SSE stream; turns the most recent plan into changes
  • POST /agent/cancel — cancel an in-flight run for a chat
  • POST /agent/dispose — release the in-memory agent for a chat (does not delete history)
  • GET /pr/status?prUrl=... — PR metadata + mergeability
  • POST /pr/merge — merge a PR

Development

git clone https://github.com/tstax/coding-tab
cd coding-tab
npm install
npm run dev   # tsup --watch
npm run lint  # tsc --noEmit
npm test

License

MIT