@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 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
+ });