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

@keenable/convex

v0.1.0

Published

Keenable web search and page fetch as a Convex component. Keyless by default.

Readme

@keenable/convex

Keenable web search and page fetch as a Convex component.

Keyless by default. Install it, use it. An API key is optional and only lifts the rate limit, so a Convex app can search the web without holding a credential at all.

Install

npm install @keenable/convex

Add the component to convex/convex.config.ts:

import { defineApp } from "convex/server";
import keenable from "@keenable/convex/convex.config";

const app = defineApp();
app.use(keenable);

export default app;

That is the whole setup. To lift the rate limit, declare the key on the app and bind it:

import { defineApp } from "convex/server";
import { v } from "convex/values";
import keenable from "@keenable/convex/convex.config";

const app = defineApp({ env: { KEENABLE_API_KEY: v.string() } });
app.use(keenable, { env: { KEENABLE_API_KEY: app.env.KEENABLE_API_KEY } });

export default app;
npx convex env set KEENABLE_API_KEY your-key

Keys come from keenable.ai/console.

Search the web

Call the component from an application-owned action. Keeping the wrapper in the app gives you a place for authentication, authorization and rate limiting.

import { KeenableClient } from "@keenable/convex";
import { v } from "convex/values";
import { action } from "./_generated/server";
import { components } from "./_generated/api";

const keenable = new KeenableClient(components.keenable);

export const searchWeb = action({
  args: { query: v.string() },
  handler: async (ctx, args) => {
    return await keenable.search(ctx, { query: args.query, maxResults: 5 });
  },
});

Each result carries title, url and snippet. snippet is the page text, not a search-engine blurb, which is why it has a character budget:

await keenable.search(ctx, {
  query: "papers on wafer-scale inference",
  site: "arxiv.org",
  publishedAfter: "2026-01-01",
  snippetMaxLength: 2_000,
  maxResults: 5,
});

Ask for the ideal page in natural language ("blog post comparing React and Vue performance") rather than typing keywords; the index is semantic.

Read one page

export const readPage = action({
  args: { url: v.string() },
  handler: async (ctx, args) => {
    return await keenable.fetch(ctx, { url: args.url, contentMaxLength: 20_000 });
  },
});

Returns the page's main content as markdown with boilerplate stripped, plus truncated so you know whether the budget cut it.

Feed a model

toContext renders results as numbered sources with their URLs, so a model can cite what it used. Sources are dropped whole once the budget is spent rather than cut mid-page.

import { KeenableClient, toContext } from "@keenable/convex";

const { results } = await keenable.search(ctx, { query: args.question });
const sources = toContext(results, { maxChars: 8_000 });

Request flow

App client
  -> application Convex action
  -> KeenableClient.search / .fetch
  -> ctx.runAction(components.keenable.lib.search / .fetchPage)
  -> POST https://api.keenable.ai/v1/search/public  (or /v1/search with a key)
     GET  https://api.keenable.ai/v1/fetch/public   (or /v1/fetch with a key)

The component is stateless and owns no database tables.

API

  • search(ctx, args)query, plus optional site, publishedAfter, publishedBefore, acquiredAfter, acquiredBefore, snippetMaxLength (default 1000) and maxResults.
  • fetch(ctx, args)url, plus optional contentMaxLength (default 10000).
  • toContext(results, { maxChars }) — a citable prompt block.

Development

npm install
npm test
npm run check
npm run build
node e2e/live.mjs   # live, keyless, hits the real API

npm run build:codegen regenerates src/component/_generated and needs a configured Convex development deployment.

License

MIT