Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/lib/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
* uses (no ad-hoc grouping in templates). Built over the enriched index.
*/
import { index } from "./data.ts";
import { allCuratedProviders, curatedProviderForDomain } from "./curated.ts";
import { readDomainCatalogTree } from "./discovered-catalog.ts";
import { discoveredDomainsByTarget } from "./discovered-domains.ts";
import { mergeDomainSurfaceCounts } from "./domain-surface-types.ts";
import { faviconUrl, isJunkDomain } from "./favicon.ts";
import { searchIndex } from "./search-index.ts";
import type { Kind } from "./types.ts";
Expand All @@ -22,6 +26,8 @@ export interface DomainSummary {

const KIND_ORDER: Kind[] = ["mcp", "openapi", "graphql", "cli"];

const discoveredByDomain = discoveredDomainsByTarget(readDomainCatalogTree().domains);

const DOMAINS: DomainSummary[] = (() => {
const map = new Map<string, DomainSummary>();
for (const r of index) {
Expand Down Expand Up @@ -55,6 +61,68 @@ const DOMAINS: DomainSummary[] = (() => {
description: entry.description,
});
}
for (const [domain, group] of map) {
const discovered = discoveredByDomain.get(domain);
const curated = curatedProviderForDomain(domain);
group.formats = mergeDomainSurfaceCounts(group.formats, {
catalog: Object.entries(group.formats)
.filter(([, count]) => (count ?? 0) > 0)
.map(([kind]) => kind as Kind),
discovered: discovered?.surfaces.map((surface) => surface.type),
curated: curated?.interfaces.map((iface) => iface.format),
});
if (!group.description) {
group.description =
discovered?.description?.replace(/\s+/g, " ").slice(0, 110) ??
curated?.description?.replace(/\s+/g, " ").slice(0, 110) ??
"";
}
}
for (const [domain, discovered] of discoveredByDomain) {
if (isJunkDomain(domain)) continue;
if (map.has(domain)) continue;
const curated = curatedProviderForDomain(domain);
map.set(domain, {
domain,
icon: faviconUrl(domain),
total: 0,
formats: mergeDomainSurfaceCounts({}, {
discovered: discovered.surfaces.map((surface) => surface.type),
curated: curated?.interfaces.map((iface) => iface.format),
}),
popularity: 0,
devtool: false,
description:
discovered.description?.replace(/\s+/g, " ").slice(0, 110) ??
curated?.description?.replace(/\s+/g, " ").slice(0, 110) ??
"",
});
}
for (const domain of [...map.keys()]) {
const curated = curatedProviderForDomain(domain);
if (!curated) continue;
const group = map.get(domain)!;
group.formats = mergeDomainSurfaceCounts(group.formats, {
catalog: Object.entries(group.formats)
.filter(([, count]) => (count ?? 0) > 0)
.map(([kind]) => kind as Kind),
curated: curated.interfaces.map((iface) => iface.format),
});
}
for (const curated of allCuratedProviders()) {
const domain = curated.domain;
if (isJunkDomain(domain)) continue;
if (map.has(domain)) continue;
map.set(domain, {
domain,
icon: faviconUrl(domain),
total: 0,
formats: mergeDomainSurfaceCounts({}, { curated: curated.interfaces.map((iface) => iface.format) }),
popularity: 0,
devtool: false,
description: curated.description.replace(/\s+/g, " ").slice(0, 110),
});
}
// Dev tools first — the audience's daily drivers — then popularity.
return [...map.values()].sort(
(a, b) => Number(b.devtool) - Number(a.devtool) || b.popularity - a.popularity || b.total - a.total || a.domain.localeCompare(b.domain),
Expand Down
73 changes: 73 additions & 0 deletions src/lib/curated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { getDomain as tldGetDomain } from "tldts";
import { canonicalDomain } from "./domain-aliases.ts";
import type { Kind } from "./types.ts";

export interface CuratedInterface {
format: Kind;
name: string;
endpoint?: string;
specUrl?: string;
auth: "oauth" | "api_key" | "token" | "none" | "mixed";
authHeader?: string;
install?: string;
docs?: string;
note?: string;
origin: "vendor" | "community";
maintainer?: string;
repo?: string;
}

export interface CuratedProvider {
slug: string;
name: string;
tagline: string;
description: string;
domain: string;
icon?: string;
categories: string[];
interfaces: CuratedInterface[];
links?: { homepage?: string; docs?: string };
}

const CURATED_DIR = join(process.cwd(), "curated");
const getDomain = (value: string) => tldGetDomain(value, { allowPrivateDomains: true });

const providers: CuratedProvider[] = (() => {
if (!existsSync(CURATED_DIR)) return [];
return readdirSync(CURATED_DIR)
.filter((file) => file.endsWith(".json"))
.map((file) => JSON.parse(readFileSync(join(CURATED_DIR, file), "utf8")) as CuratedProvider);
})();

function targetDomains(provider: CuratedProvider): string[] {
const targets = new Set<string>();
targets.add(canonicalDomain(provider.domain));
const urls = [
provider.links?.homepage,
provider.links?.docs,
...provider.interfaces.flatMap((iface) => [iface.endpoint, iface.specUrl, iface.docs, iface.repo]),
];
for (const url of urls) {
if (!url) continue;
const domain = getDomain(url);
if (domain) targets.add(canonicalDomain(domain));
}
return [...targets];
}

const byDomain = new Map<string, CuratedProvider>();
for (const provider of providers) {
for (const domain of targetDomains(provider)) {
if (domain && !byDomain.has(domain)) byDomain.set(domain, provider);
}
}

export function allCuratedProviders(): CuratedProvider[] {
return providers;
}

export function curatedProviderForDomain(domain: string): CuratedProvider | null {
return byDomain.get(canonicalDomain(domain)) ?? null;
}
51 changes: 51 additions & 0 deletions src/lib/discovered-catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { canonicalDomain } from "./domain-aliases.ts";

export interface CatalogPackage {
registryType: string;
identifier: string;
runtimeHint?: string;
}

export interface CatalogSurface {
slug: string;
name: string;
type: "http" | "graphql" | "mcp" | "cli";
url?: string;
spec?: string;
command?: string;
packages?: CatalogPackage[];
authStatus: "none" | "required" | "unknown";
}

export interface CatalogDomain {
domain: string;
description?: string;
summary: string;
discoveredAt?: string;
surfaces: CatalogSurface[];
}

export interface Catalog {
domains: CatalogDomain[];
}

const DOMAINS_DIR = join(process.cwd(), "domains");

function domainFilePath(root: string, domain: string): string {
return join(root, domain, "integrations.json");
}

export function readDomainCatalogTree(root = DOMAINS_DIR): Catalog {
if (!existsSync(root)) return { domains: [] };

const domains = readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => domainFilePath(root, entry.name))
.filter((path) => existsSync(path) && statSync(path).isFile())
.map((path) => JSON.parse(readFileSync(path, "utf8")) as CatalogDomain)
.sort((a, b) => canonicalDomain(a.domain).localeCompare(canonicalDomain(b.domain)) || a.domain.localeCompare(b.domain));

return { domains };
}
55 changes: 55 additions & 0 deletions src/lib/discovered-domains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getDomain as tldGetDomain } from "tldts";
import { canonicalDomain } from "./domain-aliases.ts";
import type { CatalogDomain } from "./discovered-catalog.ts";

const getDomain = (value: string) => tldGetDomain(value, { allowPrivateDomains: true });

export function discoveredTargetDomains(entry: CatalogDomain): string[] {
const targets = new Set<string>();
targets.add(canonicalDomain(entry.domain));
for (const surface of entry.surfaces) {
for (const value of [surface.url, surface.spec]) {
if (!value) continue;
const domain = getDomain(value);
if (domain) targets.add(canonicalDomain(domain));
}
}
return [...targets];
}

function surfaceMatchCount(entry: CatalogDomain, target: string): number {
let count = 0;
for (const surface of entry.surfaces) {
for (const value of [surface.url, surface.spec]) {
if (!value) continue;
const domain = getDomain(value);
if (domain && canonicalDomain(domain) === target) count++;
}
}
return count;
}

function outranks(target: string, next: CatalogDomain, current: CatalogDomain): boolean {
const nextExact = canonicalDomain(next.domain) === target;
const currentExact = canonicalDomain(current.domain) === target;
if (nextExact !== currentExact) return nextExact;

const nextMatches = surfaceMatchCount(next, target);
const currentMatches = surfaceMatchCount(current, target);
if (nextMatches !== currentMatches) return nextMatches > currentMatches;

if (next.surfaces.length !== current.surfaces.length) return next.surfaces.length > current.surfaces.length;

return next.domain.length < current.domain.length;
}

export function discoveredDomainsByTarget(entries: readonly CatalogDomain[]): Map<string, CatalogDomain> {
const map = new Map<string, CatalogDomain>();
for (const entry of entries) {
for (const domain of discoveredTargetDomains(entry)) {
const current = map.get(domain);
if (!current || outranks(domain, entry, current)) map.set(domain, entry);
}
}
return map;
}
57 changes: 57 additions & 0 deletions src/lib/domain-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test";
import { buildDomainDiscovery } from "./domain-discovery.ts";
import type { Integration } from "./types.ts";

describe("buildDomainDiscovery", () => {
test("merges discovered CLI surfaces into the baseline domain discovery", () => {
const records: Integration[] = [
{
id: "mcp/notion",
kind: "mcp",
slug: "notion",
name: "Notion MCP",
description: "",
categories: [],
feeds: ["openai"],
mcp: { remoteUrl: "https://mcp.notion.com/mcp" },
raw: {},
},
{
id: "openapi/notion",
kind: "openapi",
slug: "notion-api",
name: "Notion API",
description: "",
categories: [],
feeds: ["override"],
openapi: {
provider: "notion.so",
version: "1",
specUrl: "https://developers.notion.com/openapi.json",
docsUrl: "https://developers.notion.com/reference",
openapiVer: "3.1.0",
},
raw: {},
},
];

const discovered = {
domain: "notion.so",
summary: "Notion exposes APIs, MCP, and a CLI.",
surfaces: [
{
slug: "notion-cli",
name: "Notion CLI",
type: "cli" as const,
command: "ntn",
authStatus: "required" as const,
},
],
};

const doc = buildDomainDiscovery("notion.so", records, discovered, null);

expect(doc.surfaces.map((surface) => surface.type)).toEqual(["cli", "mcp", "http"]);
expect(doc.surfaces.find((surface) => surface.type === "cli")?.command).toBe("ntn");
});
});
Loading