public-api-finder 0.1.2 → 0.2.1
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.
- package/README.md +8 -1
- package/package.json +1 -1
- package/skills/public-api-finder/SKILL.md +3 -1
- package/src/cli.js +193 -13
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Find free/public APIs for agents, prototypes, demos, and integrations.
|
|
4
4
|
|
|
5
|
-
Powered by
|
|
5
|
+
Powered by multiple sources:
|
|
6
|
+
|
|
7
|
+
- [`public-api-lists/public-api-lists`](https://github.com/public-api-lists/public-api-lists) for fast curated JSON discovery
|
|
8
|
+
- [`public-apis/public-apis`](https://github.com/public-apis/public-apis) for the larger canonical README list
|
|
9
|
+
- [`APIs-guru/openapi-directory`](https://github.com/APIs-guru/openapi-directory) for OpenAPI-backed APIs
|
|
6
10
|
|
|
7
11
|
## Quick start
|
|
8
12
|
|
|
@@ -10,6 +14,7 @@ Powered by the curated [`public-api-lists/public-api-lists`](https://github.com/
|
|
|
10
14
|
npx public-api-finder "weather forecast" --no-auth --https
|
|
11
15
|
npx public-api-finder "crypto prices" --category Cryptocurrency --limit 5
|
|
12
16
|
npx public-api-finder "jobs" --json
|
|
17
|
+
npx public-api-finder "payments" --openapi
|
|
13
18
|
```
|
|
14
19
|
|
|
15
20
|
## Why
|
|
@@ -30,9 +35,11 @@ The skill tells agents to prefer the CLI first, then live-check docs/endpoints b
|
|
|
30
35
|
|
|
31
36
|
```text
|
|
32
37
|
--category <name> Filter by category substring
|
|
38
|
+
--source <name> Filter by source: public-api-lists, public-apis, apis-guru
|
|
33
39
|
--no-auth Only APIs with Auth = No
|
|
34
40
|
--https Only HTTPS APIs
|
|
35
41
|
--cors <value> Filter by CORS: Yes, No, Unknown
|
|
42
|
+
--openapi Only APIs with OpenAPI specs
|
|
36
43
|
--limit <n> Max results
|
|
37
44
|
--json Emit JSON
|
|
38
45
|
--refresh Refresh cache
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ description: Find and evaluate free/public APIs for projects, demos, agents, pro
|
|
|
5
5
|
|
|
6
6
|
# Public API Finder
|
|
7
7
|
|
|
8
|
-
Use this skill when a task needs a public API candidate. The
|
|
8
|
+
Use this skill when a task needs a public API candidate. The CLI searches multiple sources: public-api-lists, public-apis, and APIs.guru OpenAPI directory. Use the CLI first, then live-check docs/endpoints before coding.
|
|
9
9
|
|
|
10
10
|
## Quick command
|
|
11
11
|
|
|
@@ -13,6 +13,7 @@ Use this skill when a task needs a public API candidate. The agent-friendly path
|
|
|
13
13
|
npx public-api-finder "weather forecast" --no-auth --https
|
|
14
14
|
npx public-api-finder "crypto prices" --category Cryptocurrency --limit 5
|
|
15
15
|
npx public-api-finder "jobs" --json
|
|
16
|
+
npx public-api-finder "payments" --openapi
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
If npm is unavailable, use the bundled fallback script:
|
|
@@ -33,6 +34,7 @@ Recommend 2-5 APIs. Include:
|
|
|
33
34
|
- HTTPS/CORS notes
|
|
34
35
|
- One caveat to verify: rate limits, pricing, docs freshness, uptime, or terms
|
|
35
36
|
- Minimal example request only after checking docs/live endpoint
|
|
37
|
+
- OpenAPI URL when available
|
|
36
38
|
|
|
37
39
|
## Heuristics
|
|
38
40
|
|
package/src/cli.js
CHANGED
|
@@ -3,21 +3,27 @@ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const SOURCES = {
|
|
7
|
+
publicApiLists: 'https://public-api-lists.github.io/public-api-lists/api/all.json',
|
|
8
|
+
publicApisReadme: 'https://raw.githubusercontent.com/public-apis/public-apis/master/README.md',
|
|
9
|
+
apisGuru: 'https://api.apis.guru/v2/list.json',
|
|
10
|
+
};
|
|
7
11
|
const CACHE_PATH = process.env.PUBLIC_API_FINDER_CACHE || join(homedir(), '.cache', 'public-api-finder', 'all.json');
|
|
8
12
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
13
|
|
|
10
14
|
function usage() {
|
|
11
|
-
console.log(`public-api-finder —
|
|
15
|
+
console.log(`public-api-finder — multi-source public API discovery for agents
|
|
12
16
|
|
|
13
17
|
Usage:
|
|
14
18
|
public-api-finder <query> [options]
|
|
15
19
|
|
|
16
20
|
Options:
|
|
17
21
|
--category <name> Filter by category substring
|
|
22
|
+
--source <name> Filter by source: public-api-lists, public-apis, apis-guru
|
|
18
23
|
--no-auth Only APIs with Auth = No
|
|
19
24
|
--https Only HTTPS APIs
|
|
20
25
|
--cors <value> Filter by CORS: Yes, No, Unknown
|
|
26
|
+
--openapi Only APIs with OpenAPI specs
|
|
21
27
|
--limit <n> Max results (default: 8)
|
|
22
28
|
--json Emit JSON
|
|
23
29
|
--refresh Refresh cache
|
|
@@ -26,7 +32,7 @@ Options:
|
|
|
26
32
|
Examples:
|
|
27
33
|
public-api-finder "weather forecast" --no-auth --https
|
|
28
34
|
public-api-finder "crypto prices" --category Cryptocurrency --limit 5
|
|
29
|
-
public-api-finder "
|
|
35
|
+
public-api-finder "payments" --openapi --json
|
|
30
36
|
`);
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -40,7 +46,9 @@ function parseArgs(argv) {
|
|
|
40
46
|
else if (a === '--https') args.https = true;
|
|
41
47
|
else if (a === '--json') args.json = true;
|
|
42
48
|
else if (a === '--refresh') args.refresh = true;
|
|
49
|
+
else if (a === '--openapi') args.openapi = true;
|
|
43
50
|
else if (a === '--category') args.category = argv[++i] || '';
|
|
51
|
+
else if (a === '--source') args.source = argv[++i] || '';
|
|
44
52
|
else if (a === '--cors') args.cors = argv[++i] || '';
|
|
45
53
|
else if (a === '--limit') args.limit = Number(argv[++i] || 8);
|
|
46
54
|
else parts.push(a);
|
|
@@ -59,17 +67,26 @@ function intersectionCount(a, b) {
|
|
|
59
67
|
return n;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
function
|
|
70
|
+
function textScore(entry, queryTokens) {
|
|
63
71
|
const name = tokenSet(entry.name);
|
|
64
72
|
const category = tokenSet(entry.category);
|
|
65
73
|
const desc = tokenSet(entry.description);
|
|
66
|
-
const all = new Set([...name, ...category, ...desc]);
|
|
74
|
+
const all = new Set([...name, ...category, ...desc, ...tokenSet(entry.provider || '')]);
|
|
67
75
|
return 5 * intersectionCount(queryTokens, name)
|
|
68
76
|
+ 4 * intersectionCount(queryTokens, category)
|
|
69
77
|
+ 2 * intersectionCount(queryTokens, desc)
|
|
70
78
|
+ intersectionCount(queryTokens, all);
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
function score(entry, queryTokens) {
|
|
82
|
+
let base = textScore(entry, queryTokens);
|
|
83
|
+
if (entry.openapiUrl) base += 2;
|
|
84
|
+
if (entry.sources?.length > 1) base += 2;
|
|
85
|
+
if (entry.auth === 'No') base += 1;
|
|
86
|
+
if (entry.https) base += 1;
|
|
87
|
+
return base;
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
async function cacheIsFresh() {
|
|
74
91
|
try {
|
|
75
92
|
const s = await stat(CACHE_PATH);
|
|
@@ -79,28 +96,190 @@ async function cacheIsFresh() {
|
|
|
79
96
|
}
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
async function fetchJson(url) {
|
|
100
|
+
const res = await fetch(url, { headers: { 'user-agent': 'public-api-finder/0.2' } });
|
|
101
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
102
|
+
return res.json();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function fetchText(url) {
|
|
106
|
+
const res = await fetch(url, { headers: { 'user-agent': 'public-api-finder/0.2' } });
|
|
107
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
108
|
+
return res.text();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeCategory(cat) {
|
|
112
|
+
if (!cat) return 'Unknown';
|
|
113
|
+
return String(cat).replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cleanDescription(desc) {
|
|
117
|
+
return String(desc || '')
|
|
118
|
+
.replace(/<[^>]+>/g, ' ')
|
|
119
|
+
.replace(/[#*_`>\[\]()]/g, '')
|
|
120
|
+
.replace(/\s+/g, ' ')
|
|
121
|
+
.trim()
|
|
122
|
+
.slice(0, 260).replace(/\s+\S{0,20}$/, '');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeAuth(auth) {
|
|
126
|
+
const a = String(auth || 'Unknown').replace(/`/g, '').trim();
|
|
127
|
+
if (/^no$/i.test(a)) return 'No';
|
|
128
|
+
if (/api\s*key/i.test(a)) return 'apiKey';
|
|
129
|
+
if (/oauth/i.test(a)) return 'OAuth';
|
|
130
|
+
return a || 'Unknown';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parsePublicApisReadme(readme) {
|
|
134
|
+
const entries = [];
|
|
135
|
+
let category = '';
|
|
136
|
+
for (const raw of readme.split('\n')) {
|
|
137
|
+
const heading = raw.match(/^###\s+(.+)/);
|
|
138
|
+
if (heading) {
|
|
139
|
+
category = heading[1].trim();
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!raw.startsWith('| [')) continue;
|
|
143
|
+
const cells = raw.split('|').slice(1, -1).map(c => c.trim());
|
|
144
|
+
if (cells.length < 5) continue;
|
|
145
|
+
const link = cells[0].match(/\[([^\]]+)\]\(([^)]+)\)/);
|
|
146
|
+
if (!link) continue;
|
|
147
|
+
entries.push({
|
|
148
|
+
name: link[1],
|
|
149
|
+
url: link[2],
|
|
150
|
+
description: cleanDescription(cells[1]),
|
|
151
|
+
auth: normalizeAuth(cells[2]),
|
|
152
|
+
https: /^yes$/i.test(cells[3]),
|
|
153
|
+
cors: /^(yes|no|unknown)$/i.test(cells[4]) ? normalizeCategory(cells[4]) : 'Unknown',
|
|
154
|
+
category,
|
|
155
|
+
source: 'public-apis',
|
|
156
|
+
sourceWeight: 2,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return entries;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseApisGuru(data) {
|
|
163
|
+
const entries = [];
|
|
164
|
+
for (const [providerName, item] of Object.entries(data || {})) {
|
|
165
|
+
const version = item.versions?.[item.preferred] || Object.values(item.versions || {})[0];
|
|
166
|
+
const info = version?.info || {};
|
|
167
|
+
const origin = info['x-origin']?.[0]?.url;
|
|
168
|
+
entries.push({
|
|
169
|
+
name: info.title || providerName,
|
|
170
|
+
url: (info.contact?.url && !String(info.contact.url).startsWith('file:')) ? info.contact.url : ((origin && !String(origin).startsWith('file:')) ? origin : (version?.swaggerUrl || version?.openapiUrl || `https://api.apis.guru/v2/specs/${providerName}/${item.preferred || 'latest'}/openapi.json`)),
|
|
171
|
+
description: cleanDescription(info.description || `OpenAPI definition for ${providerName}`),
|
|
172
|
+
auth: 'Unknown',
|
|
173
|
+
https: true,
|
|
174
|
+
cors: 'Unknown',
|
|
175
|
+
category: normalizeCategory(info['x-apisguru-categories']?.[0] || 'OpenAPI'),
|
|
176
|
+
source: 'apis-guru',
|
|
177
|
+
sourceWeight: 2,
|
|
178
|
+
provider: providerName,
|
|
179
|
+
openapiUrl: version?.swaggerUrl || version?.openapiUrl || origin || null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return entries;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function buildData() {
|
|
186
|
+
const [pal, publicApisReadme, guru] = await Promise.allSettled([
|
|
187
|
+
fetchJson(SOURCES.publicApiLists),
|
|
188
|
+
fetchText(SOURCES.publicApisReadme),
|
|
189
|
+
fetchJson(SOURCES.apisGuru),
|
|
190
|
+
]);
|
|
191
|
+
const entries = [];
|
|
192
|
+
const sourceStatus = {};
|
|
193
|
+
if (pal.status === 'fulfilled') {
|
|
194
|
+
sourceStatus['public-api-lists'] = pal.value.entries?.length || 0;
|
|
195
|
+
entries.push(...(pal.value.entries || []).map(e => ({ ...e, auth: normalizeAuth(e.auth), source: 'public-api-lists', sourceWeight: 1 })));
|
|
196
|
+
} else sourceStatus['public-api-lists'] = `error: ${pal.reason.message}`;
|
|
197
|
+
if (publicApisReadme.status === 'fulfilled') {
|
|
198
|
+
const rows = parsePublicApisReadme(publicApisReadme.value);
|
|
199
|
+
sourceStatus['public-apis'] = rows.length;
|
|
200
|
+
entries.push(...rows);
|
|
201
|
+
} else sourceStatus['public-apis'] = `error: ${publicApisReadme.reason.message}`;
|
|
202
|
+
if (guru.status === 'fulfilled') {
|
|
203
|
+
const rows = parseApisGuru(guru.value);
|
|
204
|
+
sourceStatus['apis-guru'] = rows.length;
|
|
205
|
+
entries.push(...rows);
|
|
206
|
+
} else sourceStatus['apis-guru'] = `error: ${guru.reason.message}`;
|
|
207
|
+
return { generatedAt: new Date().toISOString(), sourceStatus, entries: dedupe(entries) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function keyFor(entry) {
|
|
211
|
+
const host = String(entry.url || '').toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0];
|
|
212
|
+
return `${String(entry.name || '').toLowerCase().replace(/[^a-z0-9]+/g, '')}|${host}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mergeEntry(a, b) {
|
|
216
|
+
const sources = new Set([...(a.sources || [a.source]), ...(b.sources || [b.source])].filter(Boolean));
|
|
217
|
+
return {
|
|
218
|
+
...a,
|
|
219
|
+
description: (b.description || '').length > (a.description || '').length ? b.description : a.description,
|
|
220
|
+
auth: a.auth !== 'Unknown' ? a.auth : b.auth,
|
|
221
|
+
https: Boolean(a.https || b.https),
|
|
222
|
+
cors: a.cors !== 'Unknown' ? a.cors : b.cors,
|
|
223
|
+
category: a.category !== 'Unknown' ? a.category : b.category,
|
|
224
|
+
openapiUrl: a.openapiUrl || b.openapiUrl || null,
|
|
225
|
+
provider: a.provider || b.provider,
|
|
226
|
+
sourceWeight: (a.sourceWeight || 0) + (b.sourceWeight || 0),
|
|
227
|
+
sources: [...sources],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function dedupe(entries) {
|
|
232
|
+
const map = new Map();
|
|
233
|
+
for (const e of entries) {
|
|
234
|
+
const clean = {
|
|
235
|
+
name: e.name,
|
|
236
|
+
url: e.url,
|
|
237
|
+
description: cleanDescription(e.description),
|
|
238
|
+
auth: normalizeAuth(e.auth),
|
|
239
|
+
https: Boolean(e.https),
|
|
240
|
+
cors: e.cors || 'Unknown',
|
|
241
|
+
category: normalizeCategory(e.category),
|
|
242
|
+
source: e.source,
|
|
243
|
+
sourceWeight: e.sourceWeight || 1,
|
|
244
|
+
sources: [...(e.sources || []), e.source].filter(Boolean),
|
|
245
|
+
provider: e.provider,
|
|
246
|
+
openapiUrl: e.openapiUrl || null,
|
|
247
|
+
};
|
|
248
|
+
const key = keyFor(clean);
|
|
249
|
+
map.set(key, map.has(key) ? mergeEntry(map.get(key), clean) : clean);
|
|
250
|
+
}
|
|
251
|
+
return [...map.values()];
|
|
252
|
+
}
|
|
253
|
+
|
|
82
254
|
async function loadData(refresh = false) {
|
|
83
255
|
if (!refresh && await cacheIsFresh()) {
|
|
84
|
-
|
|
256
|
+
const cached = JSON.parse(await readFile(CACHE_PATH, 'utf8'));
|
|
257
|
+
return cached.entries || [];
|
|
85
258
|
}
|
|
86
|
-
const
|
|
87
|
-
if (!res.ok) throw new Error(`failed to fetch API list: HTTP ${res.status}`);
|
|
88
|
-
const data = await res.json();
|
|
259
|
+
const data = await buildData();
|
|
89
260
|
await mkdir(dirname(CACHE_PATH), { recursive: true });
|
|
90
261
|
await writeFile(CACHE_PATH, JSON.stringify(data, null, 2));
|
|
91
262
|
return data.entries || [];
|
|
92
263
|
}
|
|
93
264
|
|
|
265
|
+
function sourceMatches(entry, source) {
|
|
266
|
+
if (!source) return true;
|
|
267
|
+
return (entry.sources || [entry.source]).some(s => String(s).toLowerCase() === source.toLowerCase());
|
|
268
|
+
}
|
|
269
|
+
|
|
94
270
|
function filterEntries(entries, args) {
|
|
95
271
|
const q = tokenSet(args.query);
|
|
96
272
|
return entries.flatMap(e => {
|
|
97
273
|
if (args.category && !String(e.category || '').toLowerCase().includes(args.category.toLowerCase())) return [];
|
|
274
|
+
if (args.source && !sourceMatches(e, args.source)) return [];
|
|
98
275
|
if (args.noAuth && String(e.auth || '').toLowerCase() !== 'no') return [];
|
|
99
276
|
if (args.https && !e.https) return [];
|
|
277
|
+
if (args.openapi && !e.openapiUrl) return [];
|
|
100
278
|
if (args.cors && String(e.cors || '').toLowerCase() !== args.cors.toLowerCase()) return [];
|
|
279
|
+
const matched = q.size ? textScore(e, q) : 1;
|
|
280
|
+
if (q.size && matched === 0) return [];
|
|
101
281
|
const s = q.size ? score(e, q) : 1;
|
|
102
|
-
|
|
103
|
-
return [{ ...e, score: s }];
|
|
282
|
+
return [{ ...e, score: s + (e.sourceWeight || 0) }];
|
|
104
283
|
}).sort((a, b) => b.score - a.score || String(a.category).localeCompare(String(b.category)) || String(a.name).localeCompare(String(b.name))).slice(0, args.limit);
|
|
105
284
|
}
|
|
106
285
|
|
|
@@ -110,9 +289,10 @@ function printMarkdown(rows) {
|
|
|
110
289
|
return;
|
|
111
290
|
}
|
|
112
291
|
rows.forEach((e, i) => {
|
|
113
|
-
console.log(`${i + 1}. **${e.name}** (${e.category}) — ${e.description}`);
|
|
292
|
+
console.log(`${i + 1}. **${e.name}** (${e.category}) — ${cleanDescription(e.description)}`);
|
|
114
293
|
console.log(` - URL: ${e.url}`);
|
|
115
|
-
console.log(` - Auth: \`${e.auth}\` · HTTPS: ${e.https ? 'yes' : 'no'} · CORS: ${e.cors} · score: ${e.score}`);
|
|
294
|
+
console.log(` - Auth: \`${e.auth}\` · HTTPS: ${e.https ? 'yes' : 'no'} · CORS: ${e.cors} · sources: ${((e.sources && e.sources.length) ? e.sources : [e.source || 'unknown']).join(', ')} · score: ${e.score}`);
|
|
295
|
+
if (e.openapiUrl) console.log(` - OpenAPI: ${e.openapiUrl}`);
|
|
116
296
|
});
|
|
117
297
|
}
|
|
118
298
|
|