@builtbyecho/public-api-finder 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +94 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@builtbyecho/public-api-finder",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Find free/public APIs for agents and prototypes.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -9,10 +9,11 @@ const SOURCES = {
9
9
  publicApiLists: 'https://public-api-lists.github.io/public-api-lists/api/all.json',
10
10
  publicApisReadme: 'https://raw.githubusercontent.com/public-apis/public-apis/master/README.md',
11
11
  apisGuru: 'https://api.apis.guru/v2/list.json',
12
+ apiMegaList: 'https://raw.githubusercontent.com/cporter202/API-mega-list/main/README.md',
12
13
  };
13
14
  const CACHE_PATH = process.env.PUBLIC_API_FINDER_CACHE || join(homedir(), '.cache', 'public-api-finder', 'all.json');
14
15
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
15
- const DATA_VERSION = 13;
16
+ const DATA_VERSION = 15;
16
17
 
17
18
  const ENRICHMENT_FIELDS = [
18
19
  'tags',
@@ -574,23 +575,51 @@ function normalizeAuthType(auth) {
574
575
  return value && value !== 'unknown' ? value : 'unknown';
575
576
  }
576
577
 
577
- function enrichCuratedApi(api) {
578
+ function wordsFrom(value) {
579
+ return String(value || '')
580
+ .replace(/&/g, ' and ')
581
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
582
+ .toLowerCase()
583
+ .match(/[a-z0-9]+/g) || [];
584
+ }
585
+
586
+ function categoryTags(category) {
587
+ const words = wordsFrom(category).filter(word => !['and', 'api', 'apis', 'openapi', 'unknown'].includes(word));
588
+ const tags = [];
589
+ const categoryText = String(category || '').toLowerCase();
590
+ if (categoryText && !['unknown', 'openapi'].includes(categoryText)) tags.push(categoryText.replace(/\s+/g, ' '));
591
+ for (const word of words) tags.push(word);
592
+ return tags;
593
+ }
594
+
595
+ function inferTags(api, specific = {}) {
596
+ const text = `${api.name || ''} ${api.category || ''} ${api.description || ''} ${api.url || ''} ${api.provider || ''} ${(specific.tags || []).join(' ')}`;
597
+ return INTENT_TAG_RULES.filter(([pattern]) => pattern.test(text)).map(([, tag]) => tag);
598
+ }
599
+
600
+ function enrichApiMetadata(api) {
578
601
  const categoryBase = CURATED_CATEGORY_ENRICHMENTS[api.category] || {};
579
602
  const specific = CURATED_API_ENRICHMENTS[api.name] || {};
580
- const text = `${api.name || ''} ${api.description || ''} ${(specific.tags || []).join(' ')}`;
581
- const inferredTags = INTENT_TAG_RULES.filter(([pattern]) => pattern.test(text)).map(([, tag]) => tag);
582
603
  const merged = { ...categoryBase, ...specific, ...api };
604
+ const tags = uniqueStrings([
605
+ ...(categoryBase.tags || []),
606
+ ...(specific.tags || []),
607
+ ...inferTags(api, specific),
608
+ ...(api.tags || []),
609
+ ...categoryTags(api.category),
610
+ ]);
611
+
583
612
  merged.authType = api.authType || specific.authType || categoryBase.authType || normalizeAuthType(api.auth);
584
613
  merged.providerType = api.providerType || specific.providerType || categoryBase.providerType || 'public-api';
585
- merged.tags = uniqueStrings([...(categoryBase.tags || []), ...(specific.tags || []), ...inferredTags, ...(api.tags || [])]);
586
- merged.domains = uniqueStrings([...(categoryBase.domains || []), ...(specific.domains || []), ...(api.domains || [])]);
614
+ merged.tags = tags.length ? tags : ['public api'];
615
+ merged.domains = uniqueStrings([...(categoryBase.domains || []), ...(specific.domains || []), ...(api.domains || []), ...categoryTags(api.category).slice(0, 2)]);
587
616
  merged.useCases = uniqueStrings([...(categoryBase.useCases || []), ...(specific.useCases || []), ...(api.useCases || [])]);
588
617
  merged.caveats = uniqueStrings([...(categoryBase.caveats || []), ...(specific.caveats || []), ...(api.caveats || [])]);
589
618
  merged.bestFor = api.bestFor || specific.bestFor || categoryBase.bestFor;
590
619
  return merged;
591
620
  }
592
621
 
593
- const ENRICHED_CURATED_APIS = CURATED_APIS.map(enrichCuratedApi);
622
+ const ENRICHED_CURATED_APIS = CURATED_APIS.map(enrichApiMetadata);
594
623
 
595
624
  export function getCuratedApis() {
596
625
  return ENRICHED_CURATED_APIS.map(api => ({ ...api, tags: [...(api.tags || [])], domains: [...(api.domains || [])], useCases: [...(api.useCases || [])] }));
@@ -655,7 +684,7 @@ Usage:
655
684
 
656
685
  Options:
657
686
  --category <name> Filter by category substring
658
- --source <name> Filter by source: public-api-lists, public-apis, apis-guru, curated
687
+ --source <name> Filter by source: public-api-lists, public-apis, apis-guru, api-mega-list, curated
659
688
  --no-auth Only APIs with Auth = No
660
689
  --https Only HTTPS APIs
661
690
  --cors <value> Filter by CORS: Yes, No, Unknown
@@ -705,8 +734,10 @@ function applyQueryHints(args) {
705
734
  if (!args.cors && /\b(cors|frontend-safe|browser-safe|frontend safe|browser safe)\b/.test(query)) args.cors = 'Yes';
706
735
  }
707
736
 
737
+ const SEARCH_STOPWORDS = new Set(['a', 'an', 'and', 'api', 'apis', 'for', 'from', 'in', 'no', 'of', 'on', 'or', 'the', 'to', 'with']);
738
+
708
739
  function tokenSet(text) {
709
- return new Set(String(text).toLowerCase().match(/[a-z0-9]+/g)?.filter(t => t.length > 1) || []);
740
+ return new Set(String(text).toLowerCase().match(/[a-z0-9]+/g)?.filter(t => t.length > 1 && !SEARCH_STOPWORDS.has(t)) || []);
710
741
  }
711
742
 
712
743
  function intersectionCount(a, b) {
@@ -805,6 +836,10 @@ function intentPenalty(entry, queryText) {
805
836
  if (/\b(favicon|website preview|open graph|link preview|screenshot)\b/.test(queryText) && !/\b(microlink|urlbox|favicon|website metadata|open graph|link preview|screenshot)\b/.test(text)) penalty += 120;
806
837
  }
807
838
 
839
+ if (/\b(school district|school boundary|district boundary)\b/.test(queryText) && /\b(linkedin|jobs scraper|lead|sales|recruiting)\b/.test(text)) {
840
+ penalty += 95;
841
+ }
842
+
808
843
  if (!cat.includes('cryptocurrency') && /\b(wallet address|identicon|avatar|profile picture)\b/.test(queryText) && /\b(avatar|identicon|profile picture)\b/.test(text)) {
809
844
  penalty -= 20;
810
845
  }
@@ -896,6 +931,42 @@ function parsePublicApisReadme(readme) {
896
931
  return entries;
897
932
  }
898
933
 
934
+
935
+ function parseApiMegaList(readme) {
936
+ const entries = [];
937
+ let category = '';
938
+ for (const raw of readme.split('\n')) {
939
+ const heading = raw.match(/^##\s+(.+?)\s*$/) || raw.match(/^###\s+(.+?)\s*$/);
940
+ if (heading) {
941
+ const text = heading[1].replace(/[#*_`]/g, '').trim();
942
+ if (text && !/table of contents|repository stats|star this|join my|contributing|license/i.test(text)) category = normalizeCategory(text.replace(/^\d+\.\s*/, ''));
943
+ continue;
944
+ }
945
+ if (!raw.startsWith('| [')) continue;
946
+ const cells = raw.split('|').slice(1, -1).map(c => c.trim());
947
+ if (cells.length < 2) continue;
948
+ if (/^-+$/.test(cells[0]) || /^api name$/i.test(cells[0])) continue;
949
+ const link = cells[0].match(/\[([^\]]+)\]\(([^)]+)\)/);
950
+ if (!link) continue;
951
+ const name = link[1].replace(/<[^>]+>/g, '').trim();
952
+ const url = link[2].trim();
953
+ const description = cleanDescription(cells[1] || `${name} API`);
954
+ if (!name || !/^https?:\/\//i.test(url)) continue;
955
+ entries.push({
956
+ name,
957
+ url,
958
+ description,
959
+ auth: 'Unknown',
960
+ https: /^https:/i.test(url),
961
+ cors: 'Unknown',
962
+ category: category || 'Unknown',
963
+ source: 'api-mega-list',
964
+ sourceWeight: 1,
965
+ });
966
+ }
967
+ return entries;
968
+ }
969
+
899
970
  function parseApisGuru(data) {
900
971
  const entries = [];
901
972
  for (const [providerName, item] of Object.entries(data || {})) {
@@ -920,10 +991,11 @@ function parseApisGuru(data) {
920
991
  }
921
992
 
922
993
  async function buildData() {
923
- const [pal, publicApisReadme, guru] = await Promise.allSettled([
994
+ const [pal, publicApisReadme, guru, megaList] = await Promise.allSettled([
924
995
  fetchJson(SOURCES.publicApiLists),
925
996
  fetchText(SOURCES.publicApisReadme),
926
997
  fetchJson(SOURCES.apisGuru),
998
+ fetchText(SOURCES.apiMegaList),
927
999
  ]);
928
1000
  const entries = [];
929
1001
  const sourceStatus = {};
@@ -941,9 +1013,15 @@ async function buildData() {
941
1013
  sourceStatus['apis-guru'] = rows.length;
942
1014
  entries.push(...rows);
943
1015
  } else sourceStatus['apis-guru'] = `error: ${guru.reason.message}`;
1016
+ if (megaList.status === 'fulfilled') {
1017
+ const rows = parseApiMegaList(megaList.value);
1018
+ sourceStatus['api-mega-list'] = rows.length;
1019
+ entries.push(...rows);
1020
+ } else sourceStatus['api-mega-list'] = `error: ${megaList.reason.message}`;
944
1021
  sourceStatus.curated = ENRICHED_CURATED_APIS.length;
945
1022
  entries.push(...ENRICHED_CURATED_APIS);
946
- return { dataVersion: DATA_VERSION, generatedAt: new Date().toISOString(), sourceStatus, entries: dedupe(entries) };
1023
+ const deduped = dedupe(entries).map(enrichApiMetadata);
1024
+ return { dataVersion: DATA_VERSION, generatedAt: new Date().toISOString(), sourceStatus, entries: deduped };
947
1025
  }
948
1026
 
949
1027
  function keyFor(entry) {
@@ -1006,7 +1084,7 @@ function dedupe(entries) {
1006
1084
  async function loadData(refresh = false) {
1007
1085
  if (!refresh && await cacheIsFresh()) {
1008
1086
  const cached = JSON.parse(await readFile(CACHE_PATH, 'utf8'));
1009
- if (cached.dataVersion === DATA_VERSION) return cached.entries || [];
1087
+ if (cached.dataVersion === DATA_VERSION) return (cached.entries || []).map(enrichApiMetadata);
1010
1088
  }
1011
1089
  const data = await buildData();
1012
1090
  await mkdir(dirname(CACHE_PATH), { recursive: true });
@@ -1084,6 +1162,10 @@ async function checkRows(rows) {
1084
1162
  return checked;
1085
1163
  }
1086
1164
 
1165
+ export async function buildSearchIndex(options = {}) {
1166
+ return loadData(Boolean(options.refresh));
1167
+ }
1168
+
1087
1169
  export async function searchApis(options = {}) {
1088
1170
  const args = {
1089
1171
  query: options.query || '',