@builtbyecho/public-api-finder 0.1.0
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/LICENSE +21 -0
- package/README.md +39 -0
- package/package.json +35 -0
- package/skills/public-api-finder/SKILL.md +41 -0
- package/skills/public-api-finder/scripts/search_public_apis.py +113 -0
- package/src/cli.js +134 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BuiltByEcho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Public API Finder
|
|
2
|
+
|
|
3
|
+
Find free/public APIs for agents, prototypes, demos, and integrations.
|
|
4
|
+
|
|
5
|
+
Powered by the curated [`public-api-lists/public-api-lists`](https://github.com/public-api-lists/public-api-lists) JSON dataset.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @builtbyecho/public-api-finder "weather forecast" --no-auth --https
|
|
11
|
+
npx @builtbyecho/public-api-finder "crypto prices" --category Cryptocurrency --limit 5
|
|
12
|
+
npx @builtbyecho/public-api-finder "jobs" --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
Agents often waste time wandering the web for APIs. This gives them a small, predictable first stop: search a curated list, filter by auth/HTTPS/CORS, then verify the chosen API docs before coding.
|
|
18
|
+
|
|
19
|
+
## Skill
|
|
20
|
+
|
|
21
|
+
The package includes an agent skill at:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
skills/public-api-finder/SKILL.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The skill tells agents to prefer the CLI first, then live-check docs/endpoints before building.
|
|
28
|
+
|
|
29
|
+
## CLI options
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
--category <name> Filter by category substring
|
|
33
|
+
--no-auth Only APIs with Auth = No
|
|
34
|
+
--https Only HTTPS APIs
|
|
35
|
+
--cors <value> Filter by CORS: Yes, No, Unknown
|
|
36
|
+
--limit <n> Max results
|
|
37
|
+
--json Emit JSON
|
|
38
|
+
--refresh Refresh cache
|
|
39
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@builtbyecho/public-api-finder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Find free/public APIs for agents and prototypes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"public-api-finder": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"skills/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"public-api",
|
|
22
|
+
"apis",
|
|
23
|
+
"cli",
|
|
24
|
+
"skills"
|
|
25
|
+
],
|
|
26
|
+
"author": "BuiltByEcho",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/BuiltByEcho/public-api-finder.git"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: public-api-finder
|
|
3
|
+
description: Find and evaluate free/public APIs for projects, demos, agents, prototypes, data enrichment, examples, integrations, or research. Use the simple public-api-finder CLI to choose APIs by category, auth requirements, HTTPS/CORS support, and practical fit before writing integration code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Public API Finder
|
|
7
|
+
|
|
8
|
+
Use this skill when a task needs a public API candidate. The agent-friendly path is the CLI first, then live-check docs/endpoints before coding.
|
|
9
|
+
|
|
10
|
+
## Quick command
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @builtbyecho/public-api-finder "weather forecast" --no-auth --https
|
|
14
|
+
npx @builtbyecho/public-api-finder "crypto prices" --category Cryptocurrency --limit 5
|
|
15
|
+
npx @builtbyecho/public-api-finder "jobs" --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If npm is unavailable, use the bundled fallback script:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python3 skills/public-api-finder/scripts/search_public_apis.py "weather forecast" --no-auth --https
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Resolve the fallback script path relative to this `SKILL.md`.
|
|
25
|
+
|
|
26
|
+
## Output to user
|
|
27
|
+
|
|
28
|
+
Recommend 2-5 APIs. Include:
|
|
29
|
+
|
|
30
|
+
- API name and URL
|
|
31
|
+
- What it is good for
|
|
32
|
+
- Auth requirement
|
|
33
|
+
- HTTPS/CORS notes
|
|
34
|
+
- One caveat to verify: rate limits, pricing, docs freshness, uptime, or terms
|
|
35
|
+
- Minimal example request only after checking docs/live endpoint
|
|
36
|
+
|
|
37
|
+
## Heuristics
|
|
38
|
+
|
|
39
|
+
Prefer APIs that are HTTPS-enabled, no-auth or simple API key, CORS `Yes` for frontend demos, well documented, and narrowly suited to the task.
|
|
40
|
+
|
|
41
|
+
The curated list is not a production-readiness guarantee. Always verify before building around an API.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search public-api-lists for agent-friendly API candidates."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import urllib.request
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
SOURCE_URL = "https://public-api-lists.github.io/public-api-lists/api/all.json"
|
|
16
|
+
CACHE_PATH = Path(os.environ.get("PUBLIC_API_FINDER_CACHE", "~/.cache/public-api-finder/all.json")).expanduser()
|
|
17
|
+
CACHE_TTL_SECONDS = 24 * 60 * 60
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_data(refresh: bool = False) -> list[dict[str, Any]]:
|
|
21
|
+
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
fresh = CACHE_PATH.exists() and (time.time() - CACHE_PATH.stat().st_mtime) < CACHE_TTL_SECONDS
|
|
23
|
+
if refresh or not fresh:
|
|
24
|
+
with urllib.request.urlopen(SOURCE_URL, timeout=20) as res:
|
|
25
|
+
data = json.load(res)
|
|
26
|
+
CACHE_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
27
|
+
else:
|
|
28
|
+
data = json.loads(CACHE_PATH.read_text(encoding="utf-8"))
|
|
29
|
+
return data.get("entries", [])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tokens(text: str) -> set[str]:
|
|
33
|
+
return {t for t in re.findall(r"[a-z0-9]+", text.lower()) if len(t) > 1}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def score(entry: dict[str, Any], query_tokens: set[str]) -> int:
|
|
37
|
+
hay = " ".join(str(entry.get(k, "")) for k in ("name", "description", "category"))
|
|
38
|
+
hay_tokens = tokens(hay)
|
|
39
|
+
name_tokens = tokens(str(entry.get("name", "")))
|
|
40
|
+
category_tokens = tokens(str(entry.get("category", "")))
|
|
41
|
+
desc_tokens = tokens(str(entry.get("description", "")))
|
|
42
|
+
return (
|
|
43
|
+
5 * len(query_tokens & name_tokens)
|
|
44
|
+
+ 4 * len(query_tokens & category_tokens)
|
|
45
|
+
+ 2 * len(query_tokens & desc_tokens)
|
|
46
|
+
+ len(query_tokens & hay_tokens)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def filter_entries(entries: list[dict[str, Any]], args: argparse.Namespace) -> list[dict[str, Any]]:
|
|
51
|
+
q_tokens = tokens(args.query or "")
|
|
52
|
+
out = []
|
|
53
|
+
for e in entries:
|
|
54
|
+
if args.category and args.category.lower() not in str(e.get("category", "")).lower():
|
|
55
|
+
continue
|
|
56
|
+
if args.no_auth and str(e.get("auth", "")).lower() != "no":
|
|
57
|
+
continue
|
|
58
|
+
if args.https and not e.get("https"):
|
|
59
|
+
continue
|
|
60
|
+
if args.cors and args.cors.lower() != str(e.get("cors", "")).lower():
|
|
61
|
+
continue
|
|
62
|
+
s = score(e, q_tokens) if q_tokens else 1
|
|
63
|
+
if q_tokens and s == 0:
|
|
64
|
+
continue
|
|
65
|
+
item = dict(e)
|
|
66
|
+
item["score"] = s
|
|
67
|
+
out.append(item)
|
|
68
|
+
return sorted(out, key=lambda x: (-x["score"], str(x.get("category", "")), str(x.get("name", ""))))[: args.limit]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def markdown(rows: list[dict[str, Any]]) -> str:
|
|
72
|
+
if not rows:
|
|
73
|
+
return "No matching public APIs found. Try broader terms or remove filters."
|
|
74
|
+
lines = []
|
|
75
|
+
for i, e in enumerate(rows, 1):
|
|
76
|
+
auth = e.get("auth", "Unknown")
|
|
77
|
+
https = "yes" if e.get("https") else "no"
|
|
78
|
+
cors = e.get("cors", "Unknown")
|
|
79
|
+
lines.append(
|
|
80
|
+
f"{i}. **{e.get('name')}** ({e.get('category')}) — {e.get('description')}\n"
|
|
81
|
+
f" - URL: {e.get('url')}\n"
|
|
82
|
+
f" - Auth: `{auth}` · HTTPS: {https} · CORS: {cors} · score: {e.get('score')}"
|
|
83
|
+
)
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main() -> int:
|
|
88
|
+
ap = argparse.ArgumentParser(description="Search public-api-lists for API candidates")
|
|
89
|
+
ap.add_argument("query", nargs="?", default="", help="Search terms, e.g. 'weather forecast' or 'crypto prices'")
|
|
90
|
+
ap.add_argument("--category", help="Filter by category substring")
|
|
91
|
+
ap.add_argument("--no-auth", action="store_true", help="Only APIs that list Auth as No")
|
|
92
|
+
ap.add_argument("--https", action="store_true", help="Only HTTPS APIs")
|
|
93
|
+
ap.add_argument("--cors", choices=["Yes", "No", "Unknown"], help="Filter by CORS value")
|
|
94
|
+
ap.add_argument("--limit", type=int, default=8, help="Maximum results")
|
|
95
|
+
ap.add_argument("--json", action="store_true", help="Emit JSON")
|
|
96
|
+
ap.add_argument("--refresh", action="store_true", help="Refresh local cache")
|
|
97
|
+
args = ap.parse_args()
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
rows = filter_entries(load_data(args.refresh), args)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
print(f"public-api-finder: failed to load API list: {exc}", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
if args.json:
|
|
106
|
+
print(json.dumps(rows, indent=2))
|
|
107
|
+
else:
|
|
108
|
+
print(markdown(rows))
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
raise SystemExit(main())
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const SOURCE_URL = 'https://public-api-lists.github.io/public-api-lists/api/all.json';
|
|
7
|
+
const CACHE_PATH = join(homedir(), '.cache', 'public-api-finder', 'all.json');
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.log(`public-api-finder — find public APIs for agents and prototypes
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
public-api-finder <query> [options]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--category <name> Filter by category substring
|
|
18
|
+
--no-auth Only APIs with Auth = No
|
|
19
|
+
--https Only HTTPS APIs
|
|
20
|
+
--cors <value> Filter by CORS: Yes, No, Unknown
|
|
21
|
+
--limit <n> Max results (default: 8)
|
|
22
|
+
--json Emit JSON
|
|
23
|
+
--refresh Refresh cache
|
|
24
|
+
-h, --help Show help
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
public-api-finder "weather forecast" --no-auth --https
|
|
28
|
+
public-api-finder "crypto prices" --category Cryptocurrency --limit 5
|
|
29
|
+
public-api-finder "jobs" --json
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const args = { query: '', limit: 8, json: false, refresh: false };
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (a === '-h' || a === '--help') args.help = true;
|
|
39
|
+
else if (a === '--no-auth') args.noAuth = true;
|
|
40
|
+
else if (a === '--https') args.https = true;
|
|
41
|
+
else if (a === '--json') args.json = true;
|
|
42
|
+
else if (a === '--refresh') args.refresh = true;
|
|
43
|
+
else if (a === '--category') args.category = argv[++i] || '';
|
|
44
|
+
else if (a === '--cors') args.cors = argv[++i] || '';
|
|
45
|
+
else if (a === '--limit') args.limit = Number(argv[++i] || 8);
|
|
46
|
+
else parts.push(a);
|
|
47
|
+
}
|
|
48
|
+
args.query = parts.join(' ').trim();
|
|
49
|
+
return args;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tokenSet(text) {
|
|
53
|
+
return new Set(String(text).toLowerCase().match(/[a-z0-9]+/g)?.filter(t => t.length > 1) || []);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function intersectionCount(a, b) {
|
|
57
|
+
let n = 0;
|
|
58
|
+
for (const x of a) if (b.has(x)) n++;
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function score(entry, queryTokens) {
|
|
63
|
+
const name = tokenSet(entry.name);
|
|
64
|
+
const category = tokenSet(entry.category);
|
|
65
|
+
const desc = tokenSet(entry.description);
|
|
66
|
+
const all = new Set([...name, ...category, ...desc]);
|
|
67
|
+
return 5 * intersectionCount(queryTokens, name)
|
|
68
|
+
+ 4 * intersectionCount(queryTokens, category)
|
|
69
|
+
+ 2 * intersectionCount(queryTokens, desc)
|
|
70
|
+
+ intersectionCount(queryTokens, all);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function cacheIsFresh() {
|
|
74
|
+
try {
|
|
75
|
+
const s = await stat(CACHE_PATH);
|
|
76
|
+
return Date.now() - s.mtimeMs < CACHE_TTL_MS;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadData(refresh = false) {
|
|
83
|
+
if (!refresh && await cacheIsFresh()) {
|
|
84
|
+
return JSON.parse(await readFile(CACHE_PATH, 'utf8')).entries || [];
|
|
85
|
+
}
|
|
86
|
+
const res = await fetch(SOURCE_URL);
|
|
87
|
+
if (!res.ok) throw new Error(`failed to fetch API list: HTTP ${res.status}`);
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
await mkdir(dirname(CACHE_PATH), { recursive: true });
|
|
90
|
+
await writeFile(CACHE_PATH, JSON.stringify(data, null, 2));
|
|
91
|
+
return data.entries || [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function filterEntries(entries, args) {
|
|
95
|
+
const q = tokenSet(args.query);
|
|
96
|
+
return entries.flatMap(e => {
|
|
97
|
+
if (args.category && !String(e.category || '').toLowerCase().includes(args.category.toLowerCase())) return [];
|
|
98
|
+
if (args.noAuth && String(e.auth || '').toLowerCase() !== 'no') return [];
|
|
99
|
+
if (args.https && !e.https) return [];
|
|
100
|
+
if (args.cors && String(e.cors || '').toLowerCase() !== args.cors.toLowerCase()) return [];
|
|
101
|
+
const s = q.size ? score(e, q) : 1;
|
|
102
|
+
if (q.size && s === 0) return [];
|
|
103
|
+
return [{ ...e, score: s }];
|
|
104
|
+
}).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
|
+
}
|
|
106
|
+
|
|
107
|
+
function printMarkdown(rows) {
|
|
108
|
+
if (!rows.length) {
|
|
109
|
+
console.log('No matching public APIs found. Try broader terms or remove filters.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
rows.forEach((e, i) => {
|
|
113
|
+
console.log(`${i + 1}. **${e.name}** (${e.category}) — ${e.description}`);
|
|
114
|
+
console.log(` - URL: ${e.url}`);
|
|
115
|
+
console.log(` - Auth: \`${e.auth}\` · HTTPS: ${e.https ? 'yes' : 'no'} · CORS: ${e.cors} · score: ${e.score}`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const args = parseArgs(process.argv.slice(2));
|
|
121
|
+
if (args.help || !args.query) {
|
|
122
|
+
usage();
|
|
123
|
+
return args.help ? 0 : 1;
|
|
124
|
+
}
|
|
125
|
+
const rows = filterEntries(await loadData(args.refresh), args);
|
|
126
|
+
if (args.json) console.log(JSON.stringify(rows, null, 2));
|
|
127
|
+
else printMarkdown(rows);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().then(code => process.exitCode = code).catch(err => {
|
|
132
|
+
console.error(`public-api-finder: ${err.message}`);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
});
|