pool-anything 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.
Files changed (111) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +280 -0
  3. package/bin/pool-anything.js +4 -0
  4. package/data/providers.json +38 -0
  5. package/dist/admin/branding.js +34 -0
  6. package/dist/admin/components.js +189 -0
  7. package/dist/admin/pages.js +1891 -0
  8. package/dist/admin/shell.js +129 -0
  9. package/dist/admin/tokens.js +32 -0
  10. package/dist/cli/nl.js +147 -0
  11. package/dist/cli/repl.js +470 -0
  12. package/dist/cli/ui.js +81 -0
  13. package/dist/cli.js +199 -0
  14. package/dist/common/http.js +55 -0
  15. package/dist/config/env.js +94 -0
  16. package/dist/config/paths.js +112 -0
  17. package/dist/data/providers.json +38 -0
  18. package/dist/db/index.js +20 -0
  19. package/dist/db/init.js +11 -0
  20. package/dist/logo.png +0 -0
  21. package/dist/media/capabilities.js +103 -0
  22. package/dist/middleware/auth.js +37 -0
  23. package/dist/observability/health.js +31 -0
  24. package/dist/pool/index.js +407 -0
  25. package/dist/pool/rotation.js +123 -0
  26. package/dist/proxy/dns.js +39 -0
  27. package/dist/proxy/forward.js +350 -0
  28. package/dist/public/logos/anthropic.svg +1 -0
  29. package/dist/public/logos/anyscale.svg +1 -0
  30. package/dist/public/logos/appwrite.svg +1 -0
  31. package/dist/public/logos/brave.svg +1 -0
  32. package/dist/public/logos/cartesia.svg +4 -0
  33. package/dist/public/logos/claude.svg +1 -0
  34. package/dist/public/logos/cloudflare.svg +1 -0
  35. package/dist/public/logos/cohere-command.svg +1 -0
  36. package/dist/public/logos/cohere.svg +1 -0
  37. package/dist/public/logos/deepgram.svg +1 -0
  38. package/dist/public/logos/elevenlabs.svg +1 -0
  39. package/dist/public/logos/finnhub.png +0 -0
  40. package/dist/public/logos/fireworks.svg +1 -0
  41. package/dist/public/logos/gemini.svg +1 -0
  42. package/dist/public/logos/google.svg +1 -0
  43. package/dist/public/logos/googlemaps.svg +1 -0
  44. package/dist/public/logos/groq.svg +1 -0
  45. package/dist/public/logos/huggingface.svg +1 -0
  46. package/dist/public/logos/mailgun.svg +1 -0
  47. package/dist/public/logos/manifest.json +197 -0
  48. package/dist/public/logos/mapbox.svg +1 -0
  49. package/dist/public/logos/mistral.svg +1 -0
  50. package/dist/public/logos/neon.svg +1 -0
  51. package/dist/public/logos/openai.svg +1 -0
  52. package/dist/public/logos/openrouter.svg +1 -0
  53. package/dist/public/logos/openweather.png +0 -0
  54. package/dist/public/logos/perplexity.svg +1 -0
  55. package/dist/public/logos/pool-anything.svg +111 -0
  56. package/dist/public/logos/postmark.svg +4 -0
  57. package/dist/public/logos/replicate.svg +1 -0
  58. package/dist/public/logos/resend.svg +1 -0
  59. package/dist/public/logos/scrapingbee.svg +72 -0
  60. package/dist/public/logos/sendgrid.svg +1 -0
  61. package/dist/public/logos/serper.png +0 -0
  62. package/dist/public/logos/supabase.svg +1 -0
  63. package/dist/public/logos/tavily.svg +1 -0
  64. package/dist/public/logos/together.svg +1 -0
  65. package/dist/public/logos/turso.svg +1 -0
  66. package/dist/public/logos/twilio.svg +1 -0
  67. package/dist/public/logos/upstash.svg +1 -0
  68. package/dist/router/api.js +20 -0
  69. package/dist/server.js +431 -0
  70. package/dist/upstream/index.js +137 -0
  71. package/package.json +68 -0
  72. package/public/logos/anthropic.svg +1 -0
  73. package/public/logos/anyscale.svg +1 -0
  74. package/public/logos/appwrite.svg +1 -0
  75. package/public/logos/brave.svg +1 -0
  76. package/public/logos/cartesia.svg +4 -0
  77. package/public/logos/claude.svg +1 -0
  78. package/public/logos/cloudflare.svg +1 -0
  79. package/public/logos/cohere-command.svg +1 -0
  80. package/public/logos/cohere.svg +1 -0
  81. package/public/logos/deepgram.svg +1 -0
  82. package/public/logos/elevenlabs.svg +1 -0
  83. package/public/logos/finnhub.png +0 -0
  84. package/public/logos/fireworks.svg +1 -0
  85. package/public/logos/gemini.svg +1 -0
  86. package/public/logos/google.svg +1 -0
  87. package/public/logos/googlemaps.svg +1 -0
  88. package/public/logos/groq.svg +1 -0
  89. package/public/logos/huggingface.svg +1 -0
  90. package/public/logos/mailgun.svg +1 -0
  91. package/public/logos/manifest.json +197 -0
  92. package/public/logos/mapbox.svg +1 -0
  93. package/public/logos/mistral.svg +1 -0
  94. package/public/logos/neon.svg +1 -0
  95. package/public/logos/openai.svg +1 -0
  96. package/public/logos/openrouter.svg +1 -0
  97. package/public/logos/openweather.png +0 -0
  98. package/public/logos/perplexity.svg +1 -0
  99. package/public/logos/pool-anything.svg +111 -0
  100. package/public/logos/postmark.svg +4 -0
  101. package/public/logos/replicate.svg +1 -0
  102. package/public/logos/resend.svg +1 -0
  103. package/public/logos/scrapingbee.svg +72 -0
  104. package/public/logos/sendgrid.svg +1 -0
  105. package/public/logos/serper.png +0 -0
  106. package/public/logos/supabase.svg +1 -0
  107. package/public/logos/tavily.svg +1 -0
  108. package/public/logos/together.svg +1 -0
  109. package/public/logos/turso.svg +1 -0
  110. package/public/logos/twilio.svg +1 -0
  111. package/public/logos/upstash.svg +1 -0
package/dist/cli.js ADDED
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pool-anything` — global CLI entrypoint.
4
+ *
5
+ * Parses a small set of flags, maps them onto the existing env config
6
+ * (PORT / HOST / LOCAL_DB_PATH / POOL_DATA_DIR), then boots the server.
7
+ * Env vars must be set *before* importing ./server.js because config is
8
+ * evaluated at module load.
9
+ */
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { spawn } from "node:child_process";
13
+ import { PACKAGE_ROOT } from "./config/paths.js";
14
+ function readVersion() {
15
+ try {
16
+ const raw = fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8");
17
+ const pkg = JSON.parse(raw);
18
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19
+ }
20
+ catch {
21
+ return "0.0.0";
22
+ }
23
+ }
24
+ function helpText(version) {
25
+ return `pool-anything v${version} — gather free-tier API keys into one pool, rotate through them.
26
+
27
+ Usage:
28
+ pool-anything Interactive assistant shell (default when stdin is a TTY)
29
+ pool-anything "<command>" Run one assistant command, e.g. pool-anything "list pools"
30
+ pool-anything serve [options] Start the web UI + API server
31
+ pool-anything [options] Start the server (back-compat when flags are given)
32
+
33
+ Assistant commands (also inside the shell): list pools, list providers,
34
+ list keys [for <pool>], show usage for <pool>, add [N] keys to <pool>,
35
+ create pool <name> for <provider>, delete pool <name>, watch <pool>,
36
+ next [for <pool>], consume <n> [on <pool>], serve, exit. Type '.help' in-shell.
37
+
38
+ Options (server):
39
+ -p, --port <n> Port for the web UI + API (default: 3000, or $PORT)
40
+ -H, --host <addr> Interface to bind (default: 127.0.0.1, or $HOST)
41
+ --db <path> SQLite file (default: ./data/*.db in a checkout,
42
+ ~/.pool-anything/*.db when installed globally;
43
+ or $LOCAL_DB_PATH)
44
+ --data-dir <d> Data directory holding the SQLite file (or $POOL_DATA_DIR)
45
+ --open Open the web UI in your browser on startup
46
+ -e, --exec <cmd> Run one assistant command and exit
47
+ --serve Start the server (same as 'pool-anything serve')
48
+ -h, --help Show this help
49
+ -V, --version Show version
50
+
51
+ Examples:
52
+ pool-anything
53
+ pool-anything "list pools"
54
+ echo "gsk_abc" | pool-anything --exec "add keys to groq"
55
+ pool-anything serve --port 4000 --open
56
+ pool-anything --db ./my-pool.db
57
+ PORT=4000 pool-anything
58
+
59
+ Open http://localhost:3000 once serving, search for a provider, and Gather keys.
60
+ `;
61
+ }
62
+ function parseArgs(argv) {
63
+ const opts = { open: false, help: false, version: false, serve: false, command: [] };
64
+ let serverFlagSeen = false;
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i];
67
+ if (a === "-h" || a === "--help")
68
+ opts.help = true;
69
+ else if (a === "-V" || a === "--version")
70
+ opts.version = true;
71
+ else if (a === "--open") {
72
+ opts.open = true;
73
+ serverFlagSeen = true;
74
+ }
75
+ else if (a === "--no-open")
76
+ opts.open = false;
77
+ else if (a === "--serve")
78
+ opts.serve = true;
79
+ else if (a === "-e" || a === "--exec")
80
+ opts.exec = argv[++i] ?? "";
81
+ else if (a.startsWith("--exec="))
82
+ opts.exec = a.slice("--exec=".length);
83
+ else if (a === "-p" || a === "--port") {
84
+ opts.port = argv[++i];
85
+ serverFlagSeen = true;
86
+ }
87
+ else if (a === "-H" || a === "--host") {
88
+ opts.host = argv[++i];
89
+ serverFlagSeen = true;
90
+ }
91
+ else if (a === "--db") {
92
+ opts.db = argv[++i];
93
+ serverFlagSeen = true;
94
+ }
95
+ else if (a === "--data-dir" || a === "--dataDir") {
96
+ opts.dataDir = argv[++i];
97
+ serverFlagSeen = true;
98
+ }
99
+ else if (a.startsWith("--port=")) {
100
+ opts.port = a.slice("--port=".length);
101
+ serverFlagSeen = true;
102
+ }
103
+ else if (a.startsWith("--host=")) {
104
+ opts.host = a.slice("--host=".length);
105
+ serverFlagSeen = true;
106
+ }
107
+ else if (a.startsWith("--db=")) {
108
+ opts.db = a.slice("--db=".length);
109
+ serverFlagSeen = true;
110
+ }
111
+ else if (a.startsWith("--data-dir=")) {
112
+ opts.dataDir = a.slice("--data-dir=".length);
113
+ serverFlagSeen = true;
114
+ }
115
+ else if (a.startsWith("-p") && a.length > 2) {
116
+ opts.port = a.slice(2);
117
+ serverFlagSeen = true;
118
+ }
119
+ else if ((a === "serve" || a === "server" || a === "start") && i === 0)
120
+ opts.serve = true;
121
+ else if (!a.startsWith("-"))
122
+ opts.command.push(a);
123
+ else {
124
+ console.error(`Unknown option: ${a}\n`);
125
+ console.error(helpText(readVersion()));
126
+ process.exit(1);
127
+ }
128
+ }
129
+ // Any explicit server flag implies server mode (back-compat with v0.1.0,
130
+ // where flags alone booted the server).
131
+ if (serverFlagSeen)
132
+ opts.serve = true;
133
+ return opts;
134
+ }
135
+ function openBrowser(url) {
136
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
137
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : process.platform === "darwin" ? [url] : [url];
138
+ try {
139
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
140
+ }
141
+ catch (e) {
142
+ console.warn(`[warn] could not open browser: ${e.message}`);
143
+ }
144
+ }
145
+ const opts = parseArgs(process.argv.slice(2));
146
+ const version = readVersion();
147
+ if (opts.help) {
148
+ console.log(helpText(version));
149
+ process.exit(0);
150
+ }
151
+ if (opts.version) {
152
+ console.log(version);
153
+ process.exit(0);
154
+ }
155
+ if (opts.port !== undefined) {
156
+ if (!/^\d+$/.test(opts.port) || Number(opts.port) < 1 || Number(opts.port) > 65535) {
157
+ console.error(`Invalid --port: ${opts.port} (must be 1-65535)`);
158
+ process.exit(1);
159
+ }
160
+ process.env.PORT = opts.port;
161
+ }
162
+ if (opts.host !== undefined) {
163
+ if (!opts.host) {
164
+ console.error("Invalid --host: must be non-empty");
165
+ process.exit(1);
166
+ }
167
+ process.env.HOST = opts.host;
168
+ }
169
+ if (opts.db !== undefined) {
170
+ if (!opts.db) {
171
+ console.error("Invalid --db: must be a path");
172
+ process.exit(1);
173
+ }
174
+ process.env.LOCAL_DB_PATH = opts.db;
175
+ }
176
+ if (opts.dataDir !== undefined) {
177
+ if (!opts.dataDir) {
178
+ console.error("Invalid --data-dir: must be a path");
179
+ process.exit(1);
180
+ }
181
+ process.env.POOL_DATA_DIR = opts.dataDir;
182
+ }
183
+ if (opts.open) {
184
+ const host = process.env.HOST ?? "127.0.0.1";
185
+ const port = process.env.PORT ?? "3000";
186
+ const displayHost = host === "0.0.0.0" || host === "::" ? "localhost" : host;
187
+ setTimeout(() => openBrowser(`http://${displayHost}:${port}`), 800);
188
+ }
189
+ const oneShot = opts.exec ?? (opts.command.length > 0 ? opts.command.join(" ") : "");
190
+ if (!opts.serve && oneShot) {
191
+ const { execOnce } = await import("./cli/repl.js");
192
+ process.exit(await execOnce(oneShot));
193
+ }
194
+ if (!opts.serve && process.stdin.isTTY && process.stdout.isTTY) {
195
+ const { startRepl } = await import("./cli/repl.js");
196
+ await startRepl(version);
197
+ process.exit(0);
198
+ }
199
+ await import("./server.js");
@@ -0,0 +1,55 @@
1
+ export function readJson(req, maxBytes = 1_000_000) {
2
+ return new Promise((resolve, reject) => {
3
+ let bytes = 0;
4
+ const chunks = [];
5
+ let settled = false;
6
+ const done = (fn) => {
7
+ if (settled)
8
+ return;
9
+ settled = true;
10
+ req.removeAllListeners("data");
11
+ req.removeAllListeners("end");
12
+ req.removeAllListeners("error");
13
+ req.removeAllListeners("close");
14
+ fn();
15
+ };
16
+ req.on("data", (c) => {
17
+ bytes += c.length;
18
+ if (bytes > maxBytes) {
19
+ req.destroy();
20
+ done(() => reject(new Error("too large")));
21
+ return;
22
+ }
23
+ chunks.push(c);
24
+ });
25
+ req.on("end", () => {
26
+ done(() => {
27
+ const s = Buffer.concat(chunks).toString("utf8");
28
+ if (!s)
29
+ return resolve({});
30
+ try {
31
+ resolve(JSON.parse(s));
32
+ }
33
+ catch {
34
+ reject(new Error("bad json"));
35
+ }
36
+ });
37
+ });
38
+ req.on("error", (e) => done(() => reject(e)));
39
+ req.on("close", () => done(() => reject(new Error("connection closed"))));
40
+ });
41
+ }
42
+ export function send(res, code, body) {
43
+ if (res.headersSent)
44
+ return;
45
+ res.writeHead(code, {
46
+ "content-type": "application/json; charset=utf-8",
47
+ "cache-control": "no-store",
48
+ "x-content-type-options": "nosniff",
49
+ });
50
+ res.end(JSON.stringify(body));
51
+ }
52
+ /** Full HTML-escape for values interpolated into innerHTML (quotes alone are not enough). */
53
+ export function esc(s) {
54
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
55
+ }
@@ -0,0 +1,94 @@
1
+ import { resolveDbPath } from "./paths.js";
2
+ /** Port for the web UI + API (single server). */
3
+ function parsePort() {
4
+ const n = Number(process.env.PORT ?? 3000);
5
+ if (!Number.isInteger(n) || n < 1 || n > 65535)
6
+ return 3000;
7
+ return n;
8
+ }
9
+ export const PORT = parsePort();
10
+ /**
11
+ * Interface to bind. Defaults to loopback: the API serves raw keys with no
12
+ * auth, so it must not listen on all interfaces unless the operator opts in
13
+ * behind their own auth layer (reverse proxy, VPN, firewall).
14
+ */
15
+ export const HOST = process.env.HOST ?? "127.0.0.1";
16
+ /**
17
+ * SQLite file. Precedence: LOCAL_DB_PATH > POOL_DATA_DIR > checkout-local
18
+ * default (./data/pool-anything.db) or global default (~/.pool-anything/).
19
+ * Evaluated at import — tests set LOCAL_DB_PATH before importing.
20
+ */
21
+ export const DB_PATH = resolveDbPath();
22
+ /** Proxy upstream timeout in ms. Read live so tests/CLI can override per run. */
23
+ export function proxyTimeoutMs() {
24
+ const n = Number(process.env.PROXY_TIMEOUT_MS ?? 30000);
25
+ if (!Number.isFinite(n) || n <= 0)
26
+ return 30000;
27
+ return Math.min(n, 60000);
28
+ }
29
+ /** Test/loopback escape hatch only: allows http + private hosts upstream. Never in prod. */
30
+ export function allowPrivateUpstream() {
31
+ return process.env.ALLOW_PRIVATE_UPSTREAM === "1";
32
+ }
33
+ /** Optional bearer token protecting raw keys, writes, and proxy use. Read live so tests can override. */
34
+ export function apiToken() {
35
+ return (process.env.POOL_API_TOKEN ?? "").trim();
36
+ }
37
+ export function authEnabled() {
38
+ return apiToken().length > 0;
39
+ }
40
+ /**
41
+ * Cross-origin callers permitted to read API responses in the browser.
42
+ * Default: the hosted Pages tools UI + local dev origins only — never `*`
43
+ * and never an reflected arbitrary Origin. Operators self-hosting the UI on
44
+ * another domain set ALLOWED_ORIGINS="https://ui.example.com,...".
45
+ */
46
+ const DEFAULT_ORIGINS = [
47
+ "https://pool-anything-tools.pages.dev",
48
+ "https://pool-anything.pages.dev",
49
+ ];
50
+ export function allowedOrigins() {
51
+ const raw = (process.env.ALLOWED_ORIGINS ?? "").trim();
52
+ if (!raw)
53
+ return [...DEFAULT_ORIGINS];
54
+ return raw
55
+ .split(",")
56
+ .map((s) => s.trim().replace(/\/+$/, ""))
57
+ .filter(Boolean);
58
+ }
59
+ /** True for the hosted UI, explicit allowlist entries, and http loopback dev. */
60
+ export function isAllowedOrigin(origin) {
61
+ if (allowedOrigins().includes(origin))
62
+ return true;
63
+ try {
64
+ const u = new URL(origin);
65
+ const host = u.hostname.toLowerCase();
66
+ if (u.protocol !== "http:")
67
+ return false;
68
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1")
69
+ return true;
70
+ return false;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /** Warn once at startup when running in an insecure configuration. */
77
+ export function startupWarnings() {
78
+ const warnings = [];
79
+ if (allowPrivateUpstream()) {
80
+ warnings.push("ALLOW_PRIVATE_UPSTREAM=1 is set: SSRF protections are OFF. Test/loopback only — never with real keys or a public bind.");
81
+ }
82
+ if (HOST !== "127.0.0.1" && HOST !== "localhost" && HOST !== "::1") {
83
+ if (!authEnabled()) {
84
+ warnings.push(`HOST=${HOST} binds a non-loopback interface with no auth (POOL_API_TOKEN unset). Set POOL_API_TOKEN or put it behind your own auth layer.`);
85
+ }
86
+ else {
87
+ warnings.push(`HOST=${HOST} binds a non-loopback interface. Bearer auth is enabled — ensure POOL_API_TOKEN is strong and served over TLS/VPN.`);
88
+ }
89
+ }
90
+ if (authEnabled() && apiToken().length < 16) {
91
+ warnings.push("POOL_API_TOKEN is set but short (<16 chars). Use a long random token.");
92
+ }
93
+ return warnings;
94
+ }
@@ -0,0 +1,112 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ /**
6
+ * Package-root-aware path resolution.
7
+ *
8
+ * Problem: several modules used `process.cwd()` to locate bundled assets
9
+ * (`data/providers.json`, `public/logos/*`, `src/logo.png`) and the default
10
+ * SQLite file. That works for `git clone + npm run dev` but breaks the
11
+ * moment the package is installed globally (`npm i -g`), because cwd is
12
+ * wherever the user runs `pool-anything` from — not the install directory.
13
+ *
14
+ * This module resolves everything relative to the installed package root
15
+ * (derived from `import.meta.url`), with a `process.cwd()` fallback so a
16
+ * source checkout keeps working unchanged.
17
+ */
18
+ function findPackageRoot() {
19
+ // Walk up from this file (src/config/ or dist/config/) until package.json.
20
+ try {
21
+ let dir = path.dirname(fileURLToPath(import.meta.url));
22
+ for (let i = 0; i < 6; i++) {
23
+ if (fs.existsSync(path.join(dir, "package.json")))
24
+ return dir;
25
+ const parent = path.dirname(dir);
26
+ if (parent === dir)
27
+ break;
28
+ dir = parent;
29
+ }
30
+ }
31
+ catch {
32
+ // fall through to cwd
33
+ }
34
+ return process.cwd();
35
+ }
36
+ export const PACKAGE_ROOT = findPackageRoot();
37
+ function firstExisting(...candidates) {
38
+ for (const c of candidates) {
39
+ try {
40
+ if (c && fs.existsSync(c))
41
+ return c;
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /** Installed `data/` dir (providers.json). Prefers the package, falls back to cwd checkout. */
50
+ export function dataDir() {
51
+ const fromPkg = path.join(PACKAGE_ROOT, "data");
52
+ if (fs.existsSync(path.join(fromPkg, "providers.json")))
53
+ return fromPkg;
54
+ return path.join(process.cwd(), "data");
55
+ }
56
+ export function providersJsonPath() {
57
+ const hit = firstExisting(path.join(PACKAGE_ROOT, "data", "providers.json"), path.join(PACKAGE_ROOT, "dist", "data", "providers.json"), path.join(process.cwd(), "data", "providers.json"), path.join(process.cwd(), "dist", "data", "providers.json"));
58
+ return hit ?? path.join(PACKAGE_ROOT, "data", "providers.json");
59
+ }
60
+ /** Installed `public/` dir (provider logos + manifest). */
61
+ export function publicDir() {
62
+ const candidates = [
63
+ path.join(PACKAGE_ROOT, "public"),
64
+ path.join(PACKAGE_ROOT, "dist", "public"),
65
+ path.join(process.cwd(), "public"),
66
+ path.join(process.cwd(), "dist", "public"),
67
+ ];
68
+ for (const c of candidates) {
69
+ try {
70
+ if (fs.existsSync(c))
71
+ return c;
72
+ }
73
+ catch {
74
+ // ignore
75
+ }
76
+ }
77
+ return path.join(PACKAGE_ROOT, "public");
78
+ }
79
+ export function logoPngPath() {
80
+ const hit = firstExisting(path.join(PACKAGE_ROOT, "dist", "logo.png"), path.join(PACKAGE_ROOT, "src", "logo.png"), path.join(process.cwd(), "dist", "logo.png"), path.join(process.cwd(), "src", "logo.png"));
81
+ return hit ?? path.join(PACKAGE_ROOT, "src", "logo.png");
82
+ }
83
+ export function logoManifestPath() {
84
+ const hit = firstExisting(path.join(PACKAGE_ROOT, "public", "logos", "manifest.json"), path.join(PACKAGE_ROOT, "dist", "public", "logos", "manifest.json"), path.join(process.cwd(), "public", "logos", "manifest.json"), path.join(process.cwd(), "dist", "public", "logos", "manifest.json"));
85
+ return hit ?? path.join(PACKAGE_ROOT, "public", "logos", "manifest.json");
86
+ }
87
+ function userDataDir() {
88
+ return path.join(os.homedir(), ".pool-anything");
89
+ }
90
+ function isSourceCheckout() {
91
+ return fs.existsSync(path.join(process.cwd(), "data", "providers.json"));
92
+ }
93
+ /**
94
+ * Default SQLite location (lowest precedence — LOCAL_DB_PATH / --db wins).
95
+ * - Source checkout (`cwd/data/providers.json` exists): `./data/pool-anything.db` (unchanged).
96
+ * - Global install: `~/.pool-anything/pool-anything.db` so the db is
97
+ * writable no matter where the user runs the command from, and survives upgrades.
98
+ */
99
+ export function defaultDbPath() {
100
+ const override = process.env.POOL_DATA_DIR?.trim();
101
+ if (override)
102
+ return path.join(override, "pool-anything.db");
103
+ if (isSourceCheckout())
104
+ return path.join(process.cwd(), "data", "pool-anything.db");
105
+ return path.join(userDataDir(), "pool-anything.db");
106
+ }
107
+ export function resolveDbPath() {
108
+ const explicit = process.env.LOCAL_DB_PATH?.trim();
109
+ if (explicit)
110
+ return explicit;
111
+ return defaultDbPath();
112
+ }
@@ -0,0 +1,38 @@
1
+ [
2
+ {"id":"groq","name":"Groq","baseUrl":"https://api.groq.com/openai/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"30 req/min, 14.4k req/day free (Llama 3.3 70B)","docsUrl":"https://console.groq.com/docs/openai","keyFields":["api_key"],"hint":"console.groq.com/keys, gsk_…","logo":"public/logos/groq.svg"},
3
+ {"id":"openrouter","name":"OpenRouter","baseUrl":"https://openrouter.ai/api/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":":free models $0, unfunded ~20 RPM / 200 RPD","docsUrl":"https://openrouter.ai/docs/api-reference/overview","keyFields":["api_key"],"hint":"openrouter.ai/keys, sk-or-v1-…","logo":"public/logos/openrouter.svg"},
4
+ {"id":"gemini","name":"Google Gemini","baseUrl":"https://generativelanguage.googleapis.com/v1beta","keyHeader":"x-goog-api-key","keyPrefix":"","extraHeaders":{},"quota":"15 RPM / 1500 RPD free (Flash)","docsUrl":"https://ai.google.dev/gemini-api/docs/api-key","keyFields":["api_key"],"hint":"aistudio.google.com/apikey","logo":"public/logos/gemini.svg"},
5
+ {"id":"mistral","name":"Mistral AI","baseUrl":"https://api.mistral.ai/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"trial credits, then PAYG","docsUrl":"https://docs.mistral.ai/api/","keyFields":["api_key"],"hint":"console.mistral.ai/api-keys","logo":"public/logos/mistral.svg"},
6
+ {"id":"cohere","name":"Cohere","baseUrl":"https://api.cohere.ai/compatibility/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"trial key, rate-limited","docsUrl":"https://docs.cohere.com/docs/compatibility-api","keyFields":["api_key"],"hint":"dashboard.cohere.com/api-keys","logo":"public/logos/cohere.svg"},
7
+ {"id":"fireworks","name":"Fireworks AI","baseUrl":"https://api.fireworks.ai/inference/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"$1 credit on signup","docsUrl":"https://fireworks.ai/docs/getting-started/introduction","keyFields":["api_key"],"hint":"fireworks.ai/api-keys, fw_…","logo":"public/logos/fireworks.svg"},
8
+ {"id":"huggingface","name":"Hugging Face","baseUrl":"https://router.huggingface.co/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"serverless free w/ login, hourly limits","docsUrl":"https://huggingface.co/docs/api-inference","keyFields":["api_key"],"hint":"huggingface.co/settings/tokens, hf_…","logo":"public/logos/huggingface.svg"},
9
+ {"id":"together","name":"Together AI","baseUrl":"https://api.together.ai/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"$1 credit, no permanent free","docsUrl":"https://docs.together.ai/docs/openai-api-compatibility","keyFields":["api_key"],"hint":"api.together.ai/settings/api-keys","logo":"public/logos/together.svg"},
10
+ {"id":"replicate","name":"Replicate","baseUrl":"https://api.replicate.com/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"trial ~$5, then per-second GPU","docsUrl":"https://replicate.com/docs/reference/http","keyFields":["api_key"],"hint":"replicate.com/account/api-tokens, r8_…","logo":"public/logos/replicate.svg"},
11
+ {"id":"openai","name":"OpenAI","baseUrl":"https://api.openai.com/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"PAYG, $5 min prepay","docsUrl":"https://platform.openai.com/docs/api-reference","keyFields":["api_key"],"hint":"platform.openai.com/api-keys, sk-…","logo":"public/logos/openai.svg"},
12
+ {"id":"anthropic","name":"Anthropic Claude","baseUrl":"https://api.anthropic.com/v1","keyHeader":"x-api-key","keyPrefix":"","extraHeaders":{"anthropic-version":"2023-06-01"},"quota":"$5 credit new accounts","docsUrl":"https://docs.anthropic.com/en/api/versioning","keyFields":["api_key"],"hint":"console.anthropic.com/settings/keys, sk-ant-…","logo":"public/logos/claude.svg"},
13
+ {"id":"perplexity","name":"Perplexity","baseUrl":"https://api.perplexity.ai","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"$5 credit on signup","docsUrl":"https://docs.perplexity.ai/getting-started/quickstart","keyFields":["api_key"],"hint":"perplexity.ai/settings/api, pplx-…","logo":"public/logos/perplexity.svg"},
14
+ {"id":"anyscale","name":"Anyscale Endpoints","baseUrl":"https://api.endpoints.anyscale.com/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"legacy, deprecated","docsUrl":"https://docs.anyscale.com/endpoints/overview","keyFields":["api_key"],"hint":"app.endpoints.anyscale.com/credentials","logo":"public/logos/anyscale.svg"},
15
+ {"id":"elevenlabs","name":"ElevenLabs","baseUrl":"https://api.elevenlabs.io/v1","keyHeader":"xi-api-key","keyPrefix":"","extraHeaders":{},"quota":"10k credits/mo free","docsUrl":"https://elevenlabs.io/docs","keyFields":["api_key"],"hint":"elevenlabs.io API key","logo":"public/logos/elevenlabs.svg"},
16
+ {"id":"deepgram","name":"Deepgram","baseUrl":"https://api.deepgram.com/v1","keyHeader":"Authorization","keyPrefix":"Token ","extraHeaders":{},"quota":"$200 credit on signup","docsUrl":"https://developers.deepgram.com/docs","keyFields":["api_key"],"hint":"Authorization: Token …","logo":"public/logos/deepgram.svg"},
17
+ {"id":"cartesia","name":"Cartesia","baseUrl":"https://api.cartesia.ai","keyHeader":"X-API-Key","keyPrefix":"","extraHeaders":{"Cartesia-Version":"2024-06-10"},"quota":"20,000 free tokens","docsUrl":"https://docs.cartesia.ai","keyFields":["api_key"],"hint":"Paste a Cartesia API key","logo":"public/logos/cartesia.svg"},
18
+ {"id":"tavily","name":"Tavily","baseUrl":"https://api.tavily.com","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"1,000 credits/mo free","docsUrl":"https://docs.tavily.com/documentation/api-reference/endpoint/search","keyFields":["api_key"],"hint":"app.tavily.com/home, tvly-…","logo":"public/logos/tavily.svg"},
19
+ {"id":"brave-search","name":"Brave Search","baseUrl":"https://api.search.brave.com/res/v1","keyHeader":"X-Subscription-Token","keyPrefix":"","extraHeaders":{},"quota":"2,000 queries/mo free","docsUrl":"https://brave.com/search/api/","keyFields":["api_key"],"hint":"brave.com/search/api/","logo":"public/logos/brave.svg"},
20
+ {"id":"serper","name":"Serper","baseUrl":"https://google.serper.dev","keyHeader":"X-API-KEY","keyPrefix":"","extraHeaders":{},"quota":"2,500 queries one-time","docsUrl":"https://serper.dev/docs","keyFields":["api_key"],"hint":"serper.dev API key","logo":"public/logos/serper.png"},
21
+ {"id":"scrapingbee","name":"ScrapingBee","baseUrl":"https://app.scrapingbee.com/api/v1","keyHeader":"query:api_key","keyPrefix":"","extraHeaders":{},"quota":"1,000 free credits","docsUrl":"https://www.scrapingbee.com/documentation","keyFields":["api_key"],"hint":"?api_key= query param","logo":"public/logos/scrapingbee.svg"},
22
+ {"id":"resend","name":"Resend","baseUrl":"https://api.resend.com","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"3,000/mo, 100/day free","docsUrl":"https://resend.com/docs/api-reference/introduction","keyFields":["api_key"],"hint":"resend.com/api-keys, re_…","logo":"public/logos/resend.svg"},
23
+ {"id":"sendgrid","name":"SendGrid","baseUrl":"https://api.sendgrid.com/v3","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"100/day free (60-day trial)","docsUrl":"https://docs.sendgrid.com/api-reference/","keyFields":["api_key"],"hint":"SG.… Bearer","logo":"public/logos/sendgrid.svg"},
24
+ {"id":"mailgun","name":"Mailgun","baseUrl":"https://api.mailgun.net/v3","keyHeader":"Authorization","keyPrefix":"Basic ","extraHeaders":{},"quota":"5k/3mo trial","docsUrl":"https://documentation.mailgun.com/docs/mailgun/api-reference/","keyFields":["api_key"],"hint":"Basic api:KEY, key-…","logo":"public/logos/mailgun.svg"},
25
+ {"id":"postmark","name":"Postmark","baseUrl":"https://api.postmarkapp.com","keyHeader":"X-Postmark-Server-Token","keyPrefix":"","extraHeaders":{},"quota":"100/mo trial","docsUrl":"https://postmarkapp.com/developer/api/overview","keyFields":["api_key"],"hint":"Server Token per server","logo":"public/logos/postmark.svg"},
26
+ {"id":"twilio","name":"Twilio SMS","baseUrl":"https://api.twilio.com/2010-04-01","keyHeader":"Authorization","keyPrefix":"Basic ","extraHeaders":{},"quota":"$15 credit trial","docsUrl":"https://www.twilio.com/docs/usage/api","keyFields":["accountSid","authToken"],"hint":"AC… SID + AuthToken, Basic base64(SID:Token)","logo":"public/logos/twilio.svg"},
27
+ {"id":"finnhub","name":"Finnhub","baseUrl":"https://finnhub.io/api/v1","keyHeader":"X-Finnhub-Token","keyPrefix":"","extraHeaders":{},"quota":"60/min free","docsUrl":"https://finnhub.io/docs/api","keyFields":["api_key"],"hint":"X-Finnhub-Token or ?token=","logo":"public/logos/finnhub.png"},
28
+ {"id":"mapbox","name":"Mapbox","baseUrl":"https://api.mapbox.com","keyHeader":"query:access_token","keyPrefix":"","extraHeaders":{},"quota":"100k geocode/mo, 50k loads/mo","docsUrl":"https://docs.mapbox.com/api/overview","keyFields":["api_key"],"hint":"?access_token=pk.…","logo":"public/logos/mapbox.svg"},
29
+ {"id":"googlemaps","name":"Google Maps","baseUrl":"https://maps.googleapis.com/maps/api","keyHeader":"query:key","keyPrefix":"","extraHeaders":{},"quota":"~10k/SKU/mo under $200 credit","docsUrl":"https://developers.google.com/maps/get-started","keyFields":["api_key"],"hint":"?key=AIza…","logo":"public/logos/googlemaps.svg"},
30
+ {"id":"openweather","name":"OpenWeather","baseUrl":"https://api.openweathermap.org/data/2.5","keyHeader":"query:appid","keyPrefix":"","extraHeaders":{},"quota":"1,000/day, 60/min free","docsUrl":"https://openweathermap.org/current","keyFields":["api_key"],"hint":"?appid=…","logo":"public/logos/openweather.png"},
31
+ {"id":"supabase","name":"Supabase","baseUrl":"https://<ref>.supabase.co/rest/v1","keyHeader":"apikey","keyPrefix":"","extraHeaders":{"Authorization":"Bearer <anonKey>"},"quota":"500MB DB, 5GB egress, 50k MAU","docsUrl":"https://supabase.com/docs/reference/api/start","keyFields":["url","anonKey"],"hint":"per-project URL + anon key","logo":"public/logos/supabase.svg","poolable":"conditional"},
32
+ {"id":"neon","name":"Neon Postgres","baseUrl":"https://console.neon.tech/api/v2","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"0.5GB, 100 CU-hrs/mo","docsUrl":"https://neon.com/docs/reference/api","keyFields":["api_key"],"hint":"control-plane only; data = conn string","logo":"public/logos/neon.svg","poolable":false},
33
+ {"id":"upstash","name":"Upstash Redis","baseUrl":"https://api.upstash.com/v2","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"500k cmds/mo free","docsUrl":"https://upstash.com/docs/devops/developer-api/introduction","keyFields":["api_key"],"hint":"per-db restUrl+token for data","logo":"public/logos/upstash.svg","poolable":true},
34
+ {"id":"turso","name":"Turso","baseUrl":"https://api.turso.tech/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"5-9GB, 500M reads/mo","docsUrl":"https://docs.turso.tech/api-reference/introduction","keyFields":["orgSlug","platformToken"],"hint":"per-db URL + JWT for data","logo":"public/logos/turso.svg","poolable":true},
35
+ {"id":"appwrite","name":"Appwrite","baseUrl":"https://cloud.appwrite.io/v1","keyHeader":"X-Appwrite-Key","keyPrefix":"","extraHeaders":{"X-Appwrite-Project":"<PROJECT_ID>"},"quota":"75k MAU, 2 projects free","docsUrl":"https://appwrite.io/docs/apis/rest","keyFields":["endpoint","projectId","apiKey"],"hint":"endpoint+project+key","logo":"public/logos/appwrite.svg","poolable":"conditional"},
36
+ {"id":"cloudflare","name":"Cloudflare","baseUrl":"https://api.cloudflare.com/client/v4","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"100k req/day Workers, R2 10GB","docsUrl":"https://developers.cloudflare.com/api/","keyFields":["apiToken"],"hint":"account/zone-scoped, not poolable for data","logo":"public/logos/cloudflare.svg","poolable":false},
37
+ {"id":"custom","name":"Custom","baseUrl":"","keyHeader":"X-API-Key","keyPrefix":"","extraHeaders":{},"quota":"bring your own","docsUrl":"","keyFields":["api_key"],"hint":"Anything we don't provide: name it, add keys + info.","logo":""}
38
+ ]
@@ -0,0 +1,20 @@
1
+ import { sdb } from "../pool/index.js";
2
+ export { DB_PATH } from "../pool/index.js";
3
+ // Secondary demo table (exercises the D1 path behind /api/db/ping).
4
+ // Uses the shared pool connection: one file, one handle.
5
+ sdb.exec(`
6
+ CREATE TABLE IF NOT EXISTS items (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ name TEXT NOT NULL,
9
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10
+ );
11
+ `);
12
+ export function dbPing() {
13
+ try {
14
+ sdb.prepare("SELECT COUNT(*) AS n FROM items").get();
15
+ return { ok: true };
16
+ }
17
+ catch {
18
+ return { ok: false };
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ import { sdb } from "../pool/index.js";
2
+ import { DB_PATH, dbPing } from "./index.js";
3
+ const seed = process.argv.includes("--seed");
4
+ if (seed) {
5
+ const existing = sdb.prepare("SELECT COUNT(*) AS n FROM items WHERE name = ?").get("hello-local-d1");
6
+ if (existing.n === 0) {
7
+ sdb.prepare("INSERT INTO items (name) VALUES (?)").run("hello-local-d1");
8
+ }
9
+ }
10
+ dbPing();
11
+ console.log(`local db ready at ${DB_PATH}`);
package/dist/logo.png ADDED
Binary file