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

@theinfluencecompany/ai-cmo

v0.10.0

Published

GeoVouch client — measure whether AI assistants name your brand, know when the evidence can carry a decision, and prove whether what you shipped worked. Bring an API key; the storage, the engines and the statistics are ours.

Readme

@theinfluencecompany/ai-cmo

Measure whether AI assistants name your brand when buyers ask them questions — know when that measurement can carry a decision, and prove whether what you shipped actually worked.

npm i @theinfluencecompany/ai-cmo
import { AiCmo } from "@theinfluencecompany/ai-cmo";
const cmo = new AiCmo({ apiKey: process.env.GEOVOUCH_API_KEY! });

const { engines } = await cmo.scoreboard();     // per engine, never averaged
const { actionable } = await cmo.gaps();        // only what the evidence can carry
const brief = await cmo.brief(actionable[0].question);

// …your agent writes and ships the page, in your stack …

await cmo.recordAction({ question: actionable[0].question, kind: "answer_page",
                         url: "https://you.com/blog/x", publishedAt: new Date() });

const { assets } = await cmo.lift();            // did it work?

Read a configured market

Market IDs belong to the selected project; retrieve them before choosing a market. These methods read existing observations and do not start a scan or spend provider quota.

const { markets } = await cmo.markets();
const market = markets.find((entry) => entry.id === "tw");
if (market) {
  const scoreboard = await cmo.scoreboard(market.id);
  const gaps = await cmo.gaps(market.id);
  const recommendations = await cmo.recommend(market.id);
  // A brief can explain that more evidence is needed; it is not an instruction to publish.
  const question = gaps.actionable[0]?.question ?? gaps.researchOnly[0]?.question;
  if (question) await cmo.brief(question, market.id);
  // After the corresponding page actually goes live, retain the assignment's bank version.
  if (gaps.actionable[0]) await cmo.recordAction({
    question: gaps.actionable[0].question, kind: "answer_page",
    url: "https://you.com/zh-Hant/blog/guide", publishedAt: new Date(),
    expectedBankVersion: market.measurementScope.bankVersion,
  }, market.id);
  const lift = await cmo.lift(market.id);
}

Each response identifies its measurement selection. A disabled market can retain readable history; being configured does not mean sampling is active or that enough evidence exists. An unknown market is a server refusal, never a fallback to another market. A scoped response that omits or changes the requested market is rejected by the client. Leaving out the argument preserves legacy reads and receipts whose locale and geography were not recorded. Scoped receipts require the assignment's expectedBankVersion; a changed bank is refused instead of attaching the publication to a different measurement. Receipt deduplication and lift reads use the selected scope. Keep the published date and source bank version when retrying a receipt; do not substitute the latest bank.

Read daily Search Console data

const latest = await cmo.searchConsoleDaily({ property: "sc-domain:example.com" });
const previous = await cmo.searchConsoleDaily({
  property: "sc-domain:example.com",
  day: "2026-09-17",
});

The connected property must belong to the authorized project. history contains bounded collection metadata; days contains at most the four grains for selectedDay. Omitting day selects the latest stored day on the server. Dates use Search Console's Pacific-day labels. The method reads stored final web-search observations; it neither imports Google data nor requires a Google access token. Null sync, empty results, capped coverage and failed attempts stay explicit. The report is not a separate generative-AI traffic report, and page/query rows must not be summed into site totals.

The client validates selectors and returned data with the shared schemas. HTTP failures and malformed successful responses throw AiCmoError; invalid selectors fail locally before a request is sent. The API key is sent only in the authorization header.

Read per-page evidence (0.10.0)

const evidence = await cmo.pageEvidence();                        // measurement:read, project's own window
const korea = await cmo.pageEvidence({ market: "ko-kr", days: 28, limit: 10 });
const one = await cmo.pageEvidence({ url: "https://www.example.com/blog/post/?utm_source=x" });
for (const page of evidence.pages) {
  // most unnamed citations first
  console.log(page.url, page.ai.unnamedAnswers, page.ai.topRivalsInUnnamed, page.gsc, page.bing, page.naver);
}

For each page on your own domains it returns the AI answers in the window that cited it, how many of them named you, per engine, and the rivals named in the ones that did not. ai.engines carries each engine's denominator: answers measured in the window and answersWithPageEvidence, the answers that recorded cited pages. Compare citations only with the second: answers recorded before the server started capturing pages are not backfilled. These denominators are the stored answers in this request's window, not the scoreboard's numbers, which come from each scan's saved summary. Beside that sit Search Console clicks, impressions and impressions-weighted position; Bing clicks, impressions and Bing's own position integer (its scale is undocumented, so compare it only with other Bing values); and Naver provider positions. sources carries each search source's freshness: its last snapshot, last attempt and failure, and for Bing the row dates inside the window and any later empty refresh.

Pages are ordered by unnamed citations and capped by limit (default 20, at most 50); ai.truncated says when more pages were cited. url reads one page in any spelling — the server normalizes it with the exported normalizePageUrl, the same function that keys the evidence, and refuses a URL outside the project's domains. market scopes the AI answers only: Search Console, Bing and Naver are site-level and never attributed to a market. days defaults to the selected measurement's window.

Nothing here is a zero it did not measure. A source that is not configured, never collected, has nothing dated inside the window, was only partly observed, or could not be read is unmeasured with a reason (read_failed for the last; the other sources still answer). no_rows means the source was completely collected for the window and reported no row for the page, which is not a measured zero either. When a bound cut the stored rows, lowerBound: true marks sums that may be missing rows, and a page without rows is unmeasured with beyond_read_bound. Counts are descriptive: a page cited by answers that did not name you is an observation, not a cause, and no rate or lift is computed here. The method only reads.

Bing page stats are collected daily on the server through the product's own Bing Webmaster reader; no Bing key reaches this package or GeoVouch. Until the reader is deployed, Bing reads as unmeasured.

Compatibility. Additive: 0.9.0 clients keep working against this server, and every existing response is unchanged. pageEvidence() needs this package at 0.10.0 or later (schema 0.9.0).

What this package deliberately does not expose

No database, no migrations, no vendor token, no engine list to configure, no statistical parameter to tune. Those are ours — and keeping them ours is what lets the numbers get better without you redeploying.

Three things the API refuses to blur

unmeasured is never 0. An engine that answered without retrieving has told you nothing about whether your pages were a source, so sourcedRatePct is null rather than zero. A lift verdict with no control is unmeasured, not "no effect".

Engines are never averaged. They diverge by roughly 46×, so a single blended "AI visibility score" is the mean of four different instruments.

A gap you may act on is not a gap you can see. gaps() returns two lists. actionable cleared a precision floor, sat fully below the 50% win/loss line, and passed an anytime-valid familywise threshold — so you can check every morning without inflating your error rate. researchOnly did not, and spending on it is spending on a draw.

The loop

scoreboard → gaps → brief → you act, in your own stack → recordAction → lift

For CMS and website work, your agent already has the access, the conventions and the review rules, and any connector we built would be a worse version of what you already own. The receipt is the seam — it is what makes the last step possible for work we did not do.

MIT © The Influence Company

Opt-in shared X publication (0.7.0)

socialConnections() returns non-secret account readiness and publishing capacity. publishX() submits a project-scoped intent; socialRelease(id) retrieves its durable receipt; socialReleases() returns the latest 100 project-scoped receipts for backlog reconciliation. These methods require content:read / content:publish, independent of measurement permissions. An operator must enable policy.xPublication and promote a verified account first. No OAuth tokens appear in requests or responses.

const status = await geo.socialConnections();
const account = status.connections.find(item => item.ready)?.connection;
if (status.publishing.available && account) {
  const source = { id: "release-source", title: "The exact verified article headline", url: "https://your-brand.example/blog/article", observedAt: new Date().toISOString() };
  const receipt = await geo.publishX({
    connectionId: account.id,
    idempotencyKey: "stable-source-event-key",
    text: `${source.title}\n\nSource: ${source.url}`,
    source,
  });
  // Only receipt.release.target.outcome === "delivered" proves acceptance with a real post ID.
}

The current lane validates headline, excerpt or FAQ-question formats against authoritative published article data supplied by a registered Cloudflare service binding. No user-supplied URL is fetched. Use the exported formatXPublicationCaption(source, format) so client and server share the exact rendering. It refuses unpublished articles, unmatched excerpt/question text, expired observations, unsupported extra claims and overlength text. The source observation timestamp must come from the original collection; retrying must not refresh it. Repeating an idempotency key or the same canonical source/account reads the existing receipt. Unknown provider outcomes remain held and consume capacity; no retry repeats a provider POST. Other websites need a registered source reader. Freely authored text still requires a reviewed-artifact owner. The additive 0.8.0 video lane is described below.

Reviewed native video (0.8.0)

publishXVideo({ connectionId, idempotencyKey, text, source, media: { assetId } }) requests a native video post from an already approved product asset. The source remains a published article; text must equal the asset's approved caption and end with the article URL. The server obtains bytes through the registered product reader, checks the digest, owner approval, recording provenance and duration, and verifies the account's media scope. A new Workflow performs upload, processing checks and the one-shot final post with durable receipts. The response may be pending/unknown until an exact post ID arrives; use socialRelease(id) for its status.

socialConnections().publishing.video separately reports video readiness. socialReleases({sourceUrl}) finds the bounded receipts for an exact article even when older than the global recent list. includeMedia:false requests text-only history. An unknown upload is never repeated; a known media ID can be polled without uploading again. Actual video authorization must come from the broker's grant, not a manually invented scope in a local row.

The shared ApprovedVideoEvidence, ApprovedVideoAsset and X_VIDEO_MAX_BYTES exports define the product reader contract. The reader returns only a globally approved owned asset, never an arbitrary recording, agency-private asset or URL supplied for download. Product asset approval and storage remain in the existing product library.

Naver search observations and project configuration (0.9.0)

Naver collection is off for every project until its automation policy selects it. The opt-in lives in the existing versioned policy, so enabling it is a compare-and-set updatePolicy call.

Use this package at 0.9.0 or later (schema 0.8.0) for every client that reads the project once Naver is enabled. In 0.8.0 and earlier, AutomationPolicy.discovery and DiscoveryRun are strict objects and sources is a three-value enum, so policy() (and the CLI's ready) and discoveryRun(id) throw a ZodError on a project whose policy selects Naver.

Enable it in two steps, so one collection is inspected before any cadence starts:

const cmo = new AiCmo({ apiKey: process.env.GEOVOUCH_API_KEY!, project: "prelulu" });
const current = await cmo.policy();                        // context:read
if (!current.policy) throw new Error("Save a policy (and business context) first");
const { version, policy: before } = current.policy;         // keep `before`: it is the rollback
const naver = {
  queries: ["prelulu", "AI 영상통화", "AI 캐릭터 채팅"],
  sampling: { surface: "integrated", device: "desktop", pagesPerQuery: 1, sortBy: "relevance", dateRange: "any", includeAds: false, language: "ko", saveHtml: true, openApiEnrichment: false },
  maxChargeMicros: 25_000,
  scheduled: false,                                         // step 1: explicit runs only
} as const;
const { policy: enabled } = await cmo.updatePolicy({         // policy:write
  expectedVersion: version,
  policy: {
    ...before,
    discoveryEnabled: true,
    maxConcurrentJobs: Math.max(before.maxConcurrentJobs, 2),
    discovery: { ...before.discovery, sources: [...before.discovery.sources, "naver"], naver },
  },
});
if (!enabled) throw new Error("the policy write returned no policy");
const { run } = await cmo.discover({ idempotencyKey: "naver-first-run", source: "naver" }); // discovery:run
const page = await cmo.naverObservations({ run: run.id });                                  // discovery:read
const naverRuns = await cmo.discoveryRuns({ source: "naver" });                             // discovery:read
// step 2, after inspecting `page` and the run's spend: let the trigger collect at pollIntervalMinutes
await cmo.updatePolicy({
  expectedVersion: enabled.version,
  policy: { ...enabled.policy, discovery: { ...enabled.policy.discovery, naver: { ...naver, scheduled: true } } },
});

Every sampling option is a literal: it is the reviewed sample, and a different one is refused rather than silently collected. maxChargeMicros must cover every configured page at the pinned Actor build's prices (21,050 micros for three one-page queries); a smaller ceiling is refused, because the Actor would stop part-way and the missing pages would be unobserved, not cheaper. sources must list "naver" exactly when naver is present. scheduled: false means only an explicit discover({ source: "naver" }) collects; true lets the two-hour trigger collect whenever pollIntervalMinutes has elapsed.

A queued or running collection occupies one of the project's maxConcurrentJobs slots for the few minutes it runs. The trigger admits AI measurement before Naver in each tick, so a scheduled collection never takes the slot its own tick's measurement needs; a manual collection that overlaps a tick can, which is why the example raises the limit to 2. discover() answers 409 discovery_blocked while the poll interval since the last Naver collection (manual or scheduled) has not elapsed, or when the day's budget or a concurrency slot is unavailable; read discoveryRuns({ source: "naver" }) rather than retrying.

Rollback is updatePolicy with the before document read in step 1 (at the then-current version): removing only "naver" and naver would leave discoveryEnabled: true, which keeps editorial discovery startable for keys with discovery:run. History, usage reconciliation and reads remain.

naverObservations() only reads; it never starts a collection. Each observation is one query page with its provider run, build, dataset and key-value-store identifiers and the raw-HTML key. Read a page by its status: complete (every reported row parsed), partial (valid rows kept, absence unproven) or unobserved (missing, malformed, duplicated, collected with other settings, or from an unreviewed build). Only a complete page can support "no owned destination on this page", and even then it says nothing about whether Naver indexed the site. providerRank is the Actor's position on its parse of the page, not an organic rank, and an owned match is an exact configured host or subdomain — never a title, breadcrumb, query string or unresolved Naver redirect. These snapshots are not AI answers or citations; they never enter share of voice, citation rates, action eligibility or lift. lastAttempt / lastSuccess carry page coverage, and actualMicros: null means the cost is still reserved and unknown, never zero.

projectConfig() returns the project's measurement configuration and a revision. updateProjectConfig({ expectedRevision, displayName, config }) (requires project:manage) applies only if nothing changed since that read; a stale revision answers 409 version_conflict, and a write that would delete stored fields this server version does not recognise answers 409 unrecognized_stored_fields. A field this package does not know fails locally before a request. The Naver lane matches destinations against config.brand.domains, pinned when each collection is reserved.