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.
- package/LICENSE +202 -0
- package/README.md +280 -0
- package/bin/pool-anything.js +4 -0
- package/data/providers.json +38 -0
- package/dist/admin/branding.js +34 -0
- package/dist/admin/components.js +189 -0
- package/dist/admin/pages.js +1891 -0
- package/dist/admin/shell.js +129 -0
- package/dist/admin/tokens.js +32 -0
- package/dist/cli/nl.js +147 -0
- package/dist/cli/repl.js +470 -0
- package/dist/cli/ui.js +81 -0
- package/dist/cli.js +199 -0
- package/dist/common/http.js +55 -0
- package/dist/config/env.js +94 -0
- package/dist/config/paths.js +112 -0
- package/dist/data/providers.json +38 -0
- package/dist/db/index.js +20 -0
- package/dist/db/init.js +11 -0
- package/dist/logo.png +0 -0
- package/dist/media/capabilities.js +103 -0
- package/dist/middleware/auth.js +37 -0
- package/dist/observability/health.js +31 -0
- package/dist/pool/index.js +407 -0
- package/dist/pool/rotation.js +123 -0
- package/dist/proxy/dns.js +39 -0
- package/dist/proxy/forward.js +350 -0
- package/dist/public/logos/anthropic.svg +1 -0
- package/dist/public/logos/anyscale.svg +1 -0
- package/dist/public/logos/appwrite.svg +1 -0
- package/dist/public/logos/brave.svg +1 -0
- package/dist/public/logos/cartesia.svg +4 -0
- package/dist/public/logos/claude.svg +1 -0
- package/dist/public/logos/cloudflare.svg +1 -0
- package/dist/public/logos/cohere-command.svg +1 -0
- package/dist/public/logos/cohere.svg +1 -0
- package/dist/public/logos/deepgram.svg +1 -0
- package/dist/public/logos/elevenlabs.svg +1 -0
- package/dist/public/logos/finnhub.png +0 -0
- package/dist/public/logos/fireworks.svg +1 -0
- package/dist/public/logos/gemini.svg +1 -0
- package/dist/public/logos/google.svg +1 -0
- package/dist/public/logos/googlemaps.svg +1 -0
- package/dist/public/logos/groq.svg +1 -0
- package/dist/public/logos/huggingface.svg +1 -0
- package/dist/public/logos/mailgun.svg +1 -0
- package/dist/public/logos/manifest.json +197 -0
- package/dist/public/logos/mapbox.svg +1 -0
- package/dist/public/logos/mistral.svg +1 -0
- package/dist/public/logos/neon.svg +1 -0
- package/dist/public/logos/openai.svg +1 -0
- package/dist/public/logos/openrouter.svg +1 -0
- package/dist/public/logos/openweather.png +0 -0
- package/dist/public/logos/perplexity.svg +1 -0
- package/dist/public/logos/pool-anything.svg +111 -0
- package/dist/public/logos/postmark.svg +4 -0
- package/dist/public/logos/replicate.svg +1 -0
- package/dist/public/logos/resend.svg +1 -0
- package/dist/public/logos/scrapingbee.svg +72 -0
- package/dist/public/logos/sendgrid.svg +1 -0
- package/dist/public/logos/serper.png +0 -0
- package/dist/public/logos/supabase.svg +1 -0
- package/dist/public/logos/tavily.svg +1 -0
- package/dist/public/logos/together.svg +1 -0
- package/dist/public/logos/turso.svg +1 -0
- package/dist/public/logos/twilio.svg +1 -0
- package/dist/public/logos/upstash.svg +1 -0
- package/dist/router/api.js +20 -0
- package/dist/server.js +431 -0
- package/dist/upstream/index.js +137 -0
- package/package.json +68 -0
- package/public/logos/anthropic.svg +1 -0
- package/public/logos/anyscale.svg +1 -0
- package/public/logos/appwrite.svg +1 -0
- package/public/logos/brave.svg +1 -0
- package/public/logos/cartesia.svg +4 -0
- package/public/logos/claude.svg +1 -0
- package/public/logos/cloudflare.svg +1 -0
- package/public/logos/cohere-command.svg +1 -0
- package/public/logos/cohere.svg +1 -0
- package/public/logos/deepgram.svg +1 -0
- package/public/logos/elevenlabs.svg +1 -0
- package/public/logos/finnhub.png +0 -0
- package/public/logos/fireworks.svg +1 -0
- package/public/logos/gemini.svg +1 -0
- package/public/logos/google.svg +1 -0
- package/public/logos/googlemaps.svg +1 -0
- package/public/logos/groq.svg +1 -0
- package/public/logos/huggingface.svg +1 -0
- package/public/logos/mailgun.svg +1 -0
- package/public/logos/manifest.json +197 -0
- package/public/logos/mapbox.svg +1 -0
- package/public/logos/mistral.svg +1 -0
- package/public/logos/neon.svg +1 -0
- package/public/logos/openai.svg +1 -0
- package/public/logos/openrouter.svg +1 -0
- package/public/logos/openweather.png +0 -0
- package/public/logos/perplexity.svg +1 -0
- package/public/logos/pool-anything.svg +111 -0
- package/public/logos/postmark.svg +4 -0
- package/public/logos/replicate.svg +1 -0
- package/public/logos/resend.svg +1 -0
- package/public/logos/scrapingbee.svg +72 -0
- package/public/logos/sendgrid.svg +1 -0
- package/public/logos/serper.png +0 -0
- package/public/logos/supabase.svg +1 -0
- package/public/logos/tavily.svg +1 -0
- package/public/logos/together.svg +1 -0
- package/public/logos/turso.svg +1 -0
- package/public/logos/twilio.svg +1 -0
- package/public/logos/upstash.svg +1 -0
package/dist/cli/repl.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive REPL: readline loop + command handlers over the pool SQLite DB.
|
|
3
|
+
* Reuses pool/rotation/router helpers — no duplicated quota or rotation logic.
|
|
4
|
+
*/
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { sdb, PROVIDERS, mask, poolSummary, isMultiFieldProvider, normalizeCredentials } from "../pool/index.js";
|
|
7
|
+
import { nextKey, recordUsage } from "../pool/rotation.js";
|
|
8
|
+
import { MAX_KEYS_PER_POOL, MAX_API_KEY_CHARS } from "../router/api.js";
|
|
9
|
+
import { HOST, PORT } from "../config/env.js";
|
|
10
|
+
import { parse, suggest } from "./nl.js";
|
|
11
|
+
import { banner, poolsBox, usageBlock, helpBox, maskTip, fmtFull } from "./ui.js";
|
|
12
|
+
function allPools() {
|
|
13
|
+
return sdb.prepare("SELECT id, provider, name FROM pools ORDER BY id").all();
|
|
14
|
+
}
|
|
15
|
+
/** Resolve free-text ref: numeric id, provider id, or name substring. */
|
|
16
|
+
export function resolvePool(ref) {
|
|
17
|
+
const pools = allPools();
|
|
18
|
+
if (pools.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
const t = ref.trim().toLowerCase();
|
|
21
|
+
if (!t)
|
|
22
|
+
return pools[0];
|
|
23
|
+
if (/^\d+$/.test(t)) {
|
|
24
|
+
const hit = pools.find((p) => p.id === Number(t));
|
|
25
|
+
if (hit)
|
|
26
|
+
return hit;
|
|
27
|
+
}
|
|
28
|
+
const byProvider = pools.filter((p) => p.provider.toLowerCase() === t);
|
|
29
|
+
if (byProvider.length > 0)
|
|
30
|
+
return byProvider[0];
|
|
31
|
+
const byName = pools.filter((p) => p.name.toLowerCase().includes(t));
|
|
32
|
+
if (byName.length > 0)
|
|
33
|
+
return byName[0];
|
|
34
|
+
const byProviderSub = pools.filter((p) => p.provider.toLowerCase().includes(t));
|
|
35
|
+
if (byProviderSub.length > 0)
|
|
36
|
+
return byProviderSub[0];
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function needPool(ref) {
|
|
40
|
+
const p = resolvePool(ref);
|
|
41
|
+
if (!p) {
|
|
42
|
+
console.log(ref.trim() ? `No pool matches "${ref.trim()}". Try 'list pools'.` : "No pools yet — try: create pool <name> for <provider>.");
|
|
43
|
+
}
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
function cmdListPools() {
|
|
47
|
+
const rows = sdb.prepare("SELECT id, provider, name FROM pools ORDER BY id").all().map((p) => {
|
|
48
|
+
const s = poolSummary(p.id);
|
|
49
|
+
const keyCount = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ?").get(p.id).n;
|
|
50
|
+
return {
|
|
51
|
+
id: p.id, name: p.name, provider: p.provider, keys: keyCount,
|
|
52
|
+
used: s?.usedInWindow ?? 0, quota: s?.quota ?? null, active: keyCount > 0,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
console.log(poolsBox(rows));
|
|
56
|
+
}
|
|
57
|
+
function cmdListProviders() {
|
|
58
|
+
for (const p of PROVIDERS)
|
|
59
|
+
console.log(`- ${p.id} — ${p.name} (${p.quota || "no quota"})`);
|
|
60
|
+
}
|
|
61
|
+
function cmdListKeys(ref) {
|
|
62
|
+
const pool = needPool(ref);
|
|
63
|
+
if (!pool)
|
|
64
|
+
return;
|
|
65
|
+
const rows = sdb
|
|
66
|
+
.prepare("SELECT id, label, api_key FROM pool_keys WHERE pool_id = ? ORDER BY id")
|
|
67
|
+
.all(pool.id);
|
|
68
|
+
if (rows.length === 0) {
|
|
69
|
+
console.log(`Pool #${pool.id} "${pool.name}" has no keys. Try: add keys to ${pool.provider}.`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(`Keys in pool #${pool.id} "${pool.name}":`);
|
|
73
|
+
for (const k of rows)
|
|
74
|
+
console.log(` #${k.id} ${k.label} (${mask(k.api_key)})`);
|
|
75
|
+
console.log(maskTip());
|
|
76
|
+
}
|
|
77
|
+
function cmdUsage(ref) {
|
|
78
|
+
const pool = needPool(ref);
|
|
79
|
+
if (!pool)
|
|
80
|
+
return;
|
|
81
|
+
const s = poolSummary(pool.id);
|
|
82
|
+
if (!s) {
|
|
83
|
+
console.log("Pool not found.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const cooling = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ? AND cooldown_until > ?").get(pool.id, Date.now()).n;
|
|
87
|
+
console.log(usageBlock({ id: s.id, name: s.name, provider: s.provider, used: s.usedInWindow, quota: s.quota, remaining: s.remaining, keys: s.keys, cooling }));
|
|
88
|
+
for (const k of s.perKey) {
|
|
89
|
+
const row = sdb.prepare("SELECT api_key FROM pool_keys WHERE id = ?").get(k.id);
|
|
90
|
+
console.log(` ${k.label} (${mask(row?.api_key ?? "")}): ${fmtFull(k.used)} tok`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function cmdNext(ref) {
|
|
94
|
+
const pool = needPool(ref);
|
|
95
|
+
if (!pool)
|
|
96
|
+
return;
|
|
97
|
+
const sel = nextKey(pool.id);
|
|
98
|
+
if ("error" in sel) {
|
|
99
|
+
console.log(`Error: ${sel.error}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(`Next key in pool #${pool.id}: #${sel.key_id} ${sel.label} (${sel.masked})`);
|
|
103
|
+
}
|
|
104
|
+
function cmdConsume(tokens, ref) {
|
|
105
|
+
const pool = needPool(ref);
|
|
106
|
+
if (!pool)
|
|
107
|
+
return;
|
|
108
|
+
const r = recordUsage(pool.id, tokens);
|
|
109
|
+
if ("error" in r) {
|
|
110
|
+
console.log(`Error: ${r.error}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log(`Recorded ${fmtFull(tokens)} tokens on #${r.key_id} ${r.label} (${r.masked}).`);
|
|
114
|
+
}
|
|
115
|
+
function cmdCreatePool(name, provider, ask) {
|
|
116
|
+
return (async () => {
|
|
117
|
+
let n = name.trim();
|
|
118
|
+
let pv = provider.trim().toLowerCase();
|
|
119
|
+
if (!pv)
|
|
120
|
+
pv = (await ask("Provider id (e.g. groq)? ")).trim().toLowerCase();
|
|
121
|
+
const known = PROVIDERS.find((p) => p.id === pv);
|
|
122
|
+
if (!known) {
|
|
123
|
+
console.log(`Unknown provider "${pv}". Try 'list providers'.`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!n)
|
|
127
|
+
n = (await ask(`Name for the new ${known.name} pool? `)).trim();
|
|
128
|
+
if (!n) {
|
|
129
|
+
console.log("Cancelled (empty name).");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (n.length > 80) {
|
|
133
|
+
console.log("Name must be <= 80 characters.");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const r = sdb.prepare("INSERT INTO pools (provider, name) VALUES (?, ?)").run(known.id, n);
|
|
137
|
+
console.log(`✓ Created pool #${r.lastInsertRowid} "${n}" (${known.id}). Add keys with: add keys to ${known.id}`);
|
|
138
|
+
})();
|
|
139
|
+
}
|
|
140
|
+
function cmdDeletePool(ref, ask) {
|
|
141
|
+
return (async () => {
|
|
142
|
+
const pool = needPool(ref);
|
|
143
|
+
if (!pool)
|
|
144
|
+
return;
|
|
145
|
+
const ans = (await ask(`Delete pool #${pool.id} "${pool.name}" and its keys? [y/N] `)).trim().toLowerCase();
|
|
146
|
+
if (ans !== "y" && ans !== "yes") {
|
|
147
|
+
console.log("Cancelled.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
sdb.exec("BEGIN IMMEDIATE");
|
|
151
|
+
try {
|
|
152
|
+
sdb.prepare("DELETE FROM usage WHERE pool_id = ?").run(pool.id);
|
|
153
|
+
sdb.prepare("DELETE FROM pool_keys WHERE pool_id = ?").run(pool.id);
|
|
154
|
+
sdb.prepare("DELETE FROM pools WHERE id = ?").run(pool.id);
|
|
155
|
+
sdb.exec("COMMIT");
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
try {
|
|
159
|
+
sdb.exec("ROLLBACK");
|
|
160
|
+
}
|
|
161
|
+
catch { /* already rolled back */ }
|
|
162
|
+
throw e;
|
|
163
|
+
}
|
|
164
|
+
console.log(`✓ Deleted pool #${pool.id} "${pool.name}".`);
|
|
165
|
+
})();
|
|
166
|
+
}
|
|
167
|
+
/** Insert pasted lines as keys. Returns counts. Shared by interactive + piped import. */
|
|
168
|
+
export function insertKeys(pool, rawLines) {
|
|
169
|
+
const existing = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ?").get(pool.id).n;
|
|
170
|
+
const multi = isMultiFieldProvider(pool.provider);
|
|
171
|
+
let added = 0;
|
|
172
|
+
let skipped = 0;
|
|
173
|
+
for (const raw of rawLines) {
|
|
174
|
+
if (existing + added >= MAX_KEYS_PER_POOL)
|
|
175
|
+
break;
|
|
176
|
+
if (multi) {
|
|
177
|
+
let obj;
|
|
178
|
+
try {
|
|
179
|
+
obj = JSON.parse(raw);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
skipped++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const norm = normalizeCredentials(pool.provider, obj);
|
|
186
|
+
if (!norm.ok) {
|
|
187
|
+
skipped++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const single = norm.value["apiKey"] ?? norm.value["api_key"] ?? norm.value["apiToken"] ?? Object.values(norm.value)[0] ?? "";
|
|
191
|
+
sdb.prepare("INSERT INTO pool_keys (pool_id, label, api_key, credentials) VALUES (?, ?, ?, ?)").run(pool.id, `key ${existing + added + 1}`, single, JSON.stringify(norm.value));
|
|
192
|
+
added++;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const key = raw.trim();
|
|
196
|
+
if (!key || /[\r\n\0]/.test(key)) {
|
|
197
|
+
skipped++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (key.length > MAX_API_KEY_CHARS) {
|
|
201
|
+
skipped++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const dup = sdb.prepare("SELECT id FROM pool_keys WHERE pool_id = ? AND api_key = ?").get(pool.id, key);
|
|
205
|
+
if (dup) {
|
|
206
|
+
skipped++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
sdb.prepare("INSERT INTO pool_keys (pool_id, label, api_key) VALUES (?, ?, ?)").run(pool.id, `key ${existing + added + 1}`, key);
|
|
210
|
+
added++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { added, skipped };
|
|
214
|
+
}
|
|
215
|
+
function importPrompt(pool, expected) {
|
|
216
|
+
console.log(`✓ Opening key import mode for "${pool.name}" (pool #${pool.id})`);
|
|
217
|
+
if (isMultiFieldProvider(pool.provider)) {
|
|
218
|
+
console.log("This provider needs multi-field credentials: paste one JSON object per line.");
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log("Paste API keys (one per line, empty line to finish):");
|
|
222
|
+
}
|
|
223
|
+
if (expected !== null)
|
|
224
|
+
console.log(`(expecting ~${expected} keys)`);
|
|
225
|
+
}
|
|
226
|
+
function liveStats(poolId) {
|
|
227
|
+
const reqMin = sdb.prepare("SELECT COUNT(*) AS n FROM usage WHERE pool_id = ? AND created_at >= datetime('now','-1 minute')").get(poolId).n;
|
|
228
|
+
const tokMin = sdb.prepare("SELECT COALESCE(SUM(tokens),0) AS t FROM usage WHERE pool_id = ? AND created_at >= datetime('now','-1 minute')").get(poolId).t;
|
|
229
|
+
const lat = sdb.prepare("SELECT AVG(latency_ms) AS a FROM pool_keys WHERE pool_id = ? AND latency_ms IS NOT NULL").get(poolId);
|
|
230
|
+
const perKey = sdb
|
|
231
|
+
.prepare("SELECT k.id, k.label, k.api_key AS apiKey, COALESCE(SUM(u.tokens),0) AS used FROM pool_keys k LEFT JOIN usage u ON u.key_id = k.id WHERE k.pool_id = ? GROUP BY k.id ORDER BY k.id")
|
|
232
|
+
.all(poolId);
|
|
233
|
+
return { reqMin, tokMin, avgLatency: lat.a, perKey };
|
|
234
|
+
}
|
|
235
|
+
async function cmdWatch(ref) {
|
|
236
|
+
const pool = needPool(ref);
|
|
237
|
+
if (!pool)
|
|
238
|
+
return;
|
|
239
|
+
console.log(`🔄 Pool #${pool.id} "${pool.name}" — live usage [auto-refresh 2s, press q to exit]`);
|
|
240
|
+
await new Promise((resolve) => {
|
|
241
|
+
const render = () => {
|
|
242
|
+
const st = liveStats(pool.id);
|
|
243
|
+
readline.cursorTo(process.stdout, 0, 0);
|
|
244
|
+
readline.clearScreenDown(process.stdout);
|
|
245
|
+
console.log(`🔄 Pool #${pool.id} "${pool.name}" — live usage [auto-refresh 2s, press q to exit]`);
|
|
246
|
+
console.log(` Requests: ${st.reqMin}/min │ Tokens: ${st.tokMin}/min │ Avg latency: ${st.avgLatency === null ? "—" : `${(st.avgLatency / 1000).toFixed(1)}s`}`);
|
|
247
|
+
for (const k of st.perKey)
|
|
248
|
+
console.log(` ${k.label} (${mask(k.apiKey)}): ${fmtFull(k.used)} tok`);
|
|
249
|
+
console.log(` ─────────────────────────────────────────`);
|
|
250
|
+
};
|
|
251
|
+
render();
|
|
252
|
+
const timer = setInterval(render, 2000);
|
|
253
|
+
const stdin = process.stdin;
|
|
254
|
+
const wasRaw = stdin.isTTY && typeof stdin.setRawMode === "function";
|
|
255
|
+
if (wasRaw)
|
|
256
|
+
stdin.setRawMode(true);
|
|
257
|
+
stdin.resume();
|
|
258
|
+
const onKey = (buf) => {
|
|
259
|
+
const ch = buf.toString();
|
|
260
|
+
if (ch === "q" || ch === "Q" || ch === "\u0003" || ch === "\u001b") {
|
|
261
|
+
cleanup();
|
|
262
|
+
resolve();
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const cleanup = () => {
|
|
266
|
+
clearInterval(timer);
|
|
267
|
+
stdin.off("data", onKey);
|
|
268
|
+
if (wasRaw)
|
|
269
|
+
stdin.setRawMode(false);
|
|
270
|
+
stdin.pause();
|
|
271
|
+
};
|
|
272
|
+
stdin.on("data", onKey);
|
|
273
|
+
});
|
|
274
|
+
console.log("Stopped watching.");
|
|
275
|
+
}
|
|
276
|
+
let serverStarted = false;
|
|
277
|
+
async function cmdServe() {
|
|
278
|
+
if (serverStarted) {
|
|
279
|
+
console.log(`Server already running on http://${HOST}:${PORT}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
serverStarted = true;
|
|
283
|
+
await import("../server.js");
|
|
284
|
+
}
|
|
285
|
+
export async function runLine(input, ask, hooks) {
|
|
286
|
+
const intent = parse(input);
|
|
287
|
+
switch (intent.kind) {
|
|
288
|
+
case "exit": return "exit";
|
|
289
|
+
case "help":
|
|
290
|
+
console.log(helpBox());
|
|
291
|
+
return "ok";
|
|
292
|
+
case "clear":
|
|
293
|
+
console.clear();
|
|
294
|
+
return "ok";
|
|
295
|
+
case "version": {
|
|
296
|
+
const { readFileSync } = await import("node:fs");
|
|
297
|
+
const { PACKAGE_ROOT } = await import("../config/paths.js");
|
|
298
|
+
const { join } = await import("node:path");
|
|
299
|
+
try {
|
|
300
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
301
|
+
console.log(pkg.version ?? "0.0.0");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
console.log("0.0.0");
|
|
305
|
+
}
|
|
306
|
+
return "ok";
|
|
307
|
+
}
|
|
308
|
+
case "list-pools":
|
|
309
|
+
cmdListPools();
|
|
310
|
+
return "ok";
|
|
311
|
+
case "list-providers":
|
|
312
|
+
cmdListProviders();
|
|
313
|
+
return "ok";
|
|
314
|
+
case "list-keys":
|
|
315
|
+
cmdListKeys(intent.ref);
|
|
316
|
+
return "ok";
|
|
317
|
+
case "usage":
|
|
318
|
+
cmdUsage(intent.ref);
|
|
319
|
+
return "ok";
|
|
320
|
+
case "next":
|
|
321
|
+
cmdNext(intent.ref);
|
|
322
|
+
return "ok";
|
|
323
|
+
case "consume":
|
|
324
|
+
cmdConsume(intent.tokens, intent.ref);
|
|
325
|
+
return "ok";
|
|
326
|
+
case "create-pool":
|
|
327
|
+
await cmdCreatePool(intent.name, intent.provider, ask);
|
|
328
|
+
return "ok";
|
|
329
|
+
case "delete-pool":
|
|
330
|
+
await cmdDeletePool(intent.ref, ask);
|
|
331
|
+
return "ok";
|
|
332
|
+
case "add-keys": {
|
|
333
|
+
const pool = needPool(intent.ref);
|
|
334
|
+
if (!pool)
|
|
335
|
+
return "ok";
|
|
336
|
+
importPrompt(pool, intent.count);
|
|
337
|
+
return { pool, expected: intent.count, lines: [] };
|
|
338
|
+
}
|
|
339
|
+
case "watch": {
|
|
340
|
+
hooks?.pauseInput?.();
|
|
341
|
+
try {
|
|
342
|
+
await cmdWatch(intent.ref);
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
hooks?.resumeInput?.();
|
|
346
|
+
}
|
|
347
|
+
return "ok";
|
|
348
|
+
}
|
|
349
|
+
case "serve":
|
|
350
|
+
await cmdServe();
|
|
351
|
+
return "ok";
|
|
352
|
+
case "unknown": {
|
|
353
|
+
const s = suggest(input, PROVIDERS.map((p) => p.id));
|
|
354
|
+
console.log(`I didn't understand that.${s ? ` Did you mean '${s}'?` : ""} Type '.help' for commands.`);
|
|
355
|
+
return "ok";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
export async function startRepl(version) {
|
|
360
|
+
console.log(banner(version));
|
|
361
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "pool-anything > " });
|
|
362
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
363
|
+
let importing = null;
|
|
364
|
+
rl.prompt();
|
|
365
|
+
rl.on("line", (raw) => {
|
|
366
|
+
void (async () => {
|
|
367
|
+
// Key-import mode: collect pasted lines until blank / .done / Ctrl+D (close).
|
|
368
|
+
if (importing) {
|
|
369
|
+
if (raw.trim() === "" || raw.trim() === ".done") {
|
|
370
|
+
const st = importing;
|
|
371
|
+
importing = null;
|
|
372
|
+
if (st.lines.length === 0) {
|
|
373
|
+
console.log("No keys pasted — nothing added.");
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const { added, skipped } = insertKeys(st.pool, st.lines);
|
|
377
|
+
console.log(`✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${st.pool.id}${skipped > 0 ? ` (${skipped} skipped: blank/duplicate/invalid)` : ""}.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
importing.lines.push(raw);
|
|
382
|
+
if (importing.expected !== null && importing.lines.length >= importing.expected) {
|
|
383
|
+
const st = importing;
|
|
384
|
+
importing = null;
|
|
385
|
+
const { added, skipped } = insertKeys(st.pool, st.lines);
|
|
386
|
+
console.log(`✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${st.pool.id}${skipped > 0 ? ` (${skipped} skipped: blank/duplicate/invalid)` : ""}.`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
rl.prompt();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
rl.prompt();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const done = await runLine(raw, ask, { pauseInput: () => rl.pause(), resumeInput: () => rl.resume() });
|
|
398
|
+
if (typeof done === "object") {
|
|
399
|
+
importing = done;
|
|
400
|
+
rl.setPrompt("... ");
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
rl.setPrompt("pool-anything > ");
|
|
404
|
+
if (done === "exit")
|
|
405
|
+
rl.close();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
console.log(`Error: ${e.message}`);
|
|
410
|
+
}
|
|
411
|
+
if (rl.closed !== true)
|
|
412
|
+
rl.prompt();
|
|
413
|
+
})();
|
|
414
|
+
});
|
|
415
|
+
rl.on("close", () => {
|
|
416
|
+
// Ctrl+D during import finishes with what was pasted.
|
|
417
|
+
if (importing && importing.lines.length > 0) {
|
|
418
|
+
const { added, skipped } = insertKeys(importing.pool, importing.lines);
|
|
419
|
+
console.log(`\n✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${importing.pool.id}${skipped > 0 ? ` (${skipped} skipped)` : ""}.`);
|
|
420
|
+
importing = null;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
await new Promise((resolve) => rl.on("close", () => resolve()));
|
|
424
|
+
console.log("Goodbye!");
|
|
425
|
+
}
|
|
426
|
+
/** One-shot: run a single NL command without the interactive loop. */
|
|
427
|
+
export async function execOnce(command) {
|
|
428
|
+
const intent = parse(command);
|
|
429
|
+
// Non-interactive key import reads pasted keys from piped stdin.
|
|
430
|
+
if (intent.kind === "add-keys") {
|
|
431
|
+
const pool = needPool(intent.ref);
|
|
432
|
+
if (!pool)
|
|
433
|
+
return 1;
|
|
434
|
+
if (process.stdin.isTTY) {
|
|
435
|
+
console.log("Interactive paste needs a TTY — run `pool-anything` and type the command there, or pipe keys via stdin.");
|
|
436
|
+
return 1;
|
|
437
|
+
}
|
|
438
|
+
const text = await new Promise((resolve) => {
|
|
439
|
+
let buf = "";
|
|
440
|
+
process.stdin.setEncoding("utf8");
|
|
441
|
+
process.stdin.on("data", (c) => { buf += c; });
|
|
442
|
+
process.stdin.on("end", () => resolve(buf));
|
|
443
|
+
});
|
|
444
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
445
|
+
if (lines.length === 0) {
|
|
446
|
+
console.log("No keys on stdin — nothing added.");
|
|
447
|
+
return 1;
|
|
448
|
+
}
|
|
449
|
+
const { added, skipped } = insertKeys(pool, lines);
|
|
450
|
+
console.log(`✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${pool.id}${skipped > 0 ? ` (${skipped} skipped)` : ""}.`);
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
454
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
455
|
+
try {
|
|
456
|
+
const r = await runLine(command, ask);
|
|
457
|
+
if (typeof r === "object") {
|
|
458
|
+
console.log("Key import needs an interactive shell — run `pool-anything` without arguments.");
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
console.error(`Error: ${e.message}`);
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
rl.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Box/table rendering for the REPL. Zero dependencies; pure functions (testable). */
|
|
2
|
+
export function fmtFull(n) {
|
|
3
|
+
return Math.round(n).toLocaleString("en-US");
|
|
4
|
+
}
|
|
5
|
+
/** 12450 -> "12.4k", 28000 -> "28k", 999 -> "999", 1.5M handled. */
|
|
6
|
+
export function fmtCompact(n) {
|
|
7
|
+
if (n < 1000)
|
|
8
|
+
return String(Math.round(n));
|
|
9
|
+
if (n < 1_000_000) {
|
|
10
|
+
const k = n / 1000;
|
|
11
|
+
return `${k >= 100 ? Math.round(k) : Math.round(k * 10) / 10}k`;
|
|
12
|
+
}
|
|
13
|
+
const m = n / 1_000_000;
|
|
14
|
+
return `${m >= 100 ? Math.round(m) : Math.round(m * 10) / 10}M`;
|
|
15
|
+
}
|
|
16
|
+
function box(lines) {
|
|
17
|
+
const width = Math.max(...lines.map((l) => l.length), 0);
|
|
18
|
+
const pad = (l) => `│ ${l.padEnd(width)} │`;
|
|
19
|
+
return [`┌${"─".repeat(width + 2)}┐`, ...lines.map(pad), `└${"─".repeat(width + 2)}┘`].join("\n");
|
|
20
|
+
}
|
|
21
|
+
export function banner(version) {
|
|
22
|
+
return [
|
|
23
|
+
`╔${"═".repeat(62)}╗`,
|
|
24
|
+
`║ ${`pool-anything v${version} — API key pooler`.padEnd(60)} ║`,
|
|
25
|
+
`║ ${`Type '.help' for commands, 'exit' to quit`.padEnd(60)} ║`,
|
|
26
|
+
`╚${"═".repeat(62)}╝`,
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
export function poolsBox(pools) {
|
|
30
|
+
if (pools.length === 0)
|
|
31
|
+
return "(no pools yet — try: create pool <name> for <provider>)";
|
|
32
|
+
const header = `${pools.length} active pool${pools.length === 1 ? "" : "s"}`;
|
|
33
|
+
const lines = pools.map((p) => {
|
|
34
|
+
const usage = p.quota === null ? `${fmtCompact(p.used)} tokens` : `${fmtCompact(p.used)} / ${fmtCompact(p.quota * p.keys)} tokens`;
|
|
35
|
+
const state = p.active ? "" : ", inactive";
|
|
36
|
+
return `#${p.id} "${p.name}" (${p.provider}) — ${p.keys} key${p.keys === 1 ? "" : "s"}, ${usage}${state}`;
|
|
37
|
+
});
|
|
38
|
+
const width = Math.max(header.length, ...lines.map((l) => l.length));
|
|
39
|
+
const out = [`┌${"─".repeat(width + 2)}┐`, `│ ${header.padEnd(width)} │`, `├${"─".repeat(width + 2)}┤`];
|
|
40
|
+
for (const l of lines)
|
|
41
|
+
out.push(`│ ${l.padEnd(width)} │`);
|
|
42
|
+
out.push(`└${"─".repeat(width + 2)}┘`);
|
|
43
|
+
return out.join("\n");
|
|
44
|
+
}
|
|
45
|
+
export function usageBlock(u) {
|
|
46
|
+
const bar = "━".repeat(45);
|
|
47
|
+
const quota = u.quota === null ? "unlimited" : fmtFull(u.quota * u.keys);
|
|
48
|
+
const remaining = u.remaining === null ? "—" : fmtFull(u.remaining);
|
|
49
|
+
const pct = u.quota === null || u.quota * u.keys === 0 ? "" : ` (${Math.round((u.used / (u.quota * u.keys)) * 100)}% exhausted)`;
|
|
50
|
+
return [
|
|
51
|
+
`Pool #${u.id}: ${u.name} (${u.provider})`,
|
|
52
|
+
bar,
|
|
53
|
+
` Used: ${fmtFull(u.used)} tokens | Quota: ${quota} | Remaining: ${remaining}${pct}`,
|
|
54
|
+
` Status: ${u.cooling > 0 ? `${u.cooling} key${u.cooling === 1 ? "" : "s"} cooling` : "OK"} | Keys: ${u.keys} active, ${u.cooling} cooling`,
|
|
55
|
+
bar,
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
export function helpBox() {
|
|
59
|
+
const rows = [
|
|
60
|
+
["list pools", "show all pools with key counts + usage"],
|
|
61
|
+
["list providers", "show known providers"],
|
|
62
|
+
["list keys [for <pool>]", "masked keys in a pool"],
|
|
63
|
+
["show usage for <pool>", "token usage, quota, per-key breakdown"],
|
|
64
|
+
["add [N] keys to <pool>", "paste keys (one per line, empty line to finish)"],
|
|
65
|
+
["create pool <name> for <provider>", "make a new pool"],
|
|
66
|
+
["delete pool <name>", "remove a pool (asks to confirm)"],
|
|
67
|
+
["watch <pool>", "live usage, refreshes every 2s (q to exit)"],
|
|
68
|
+
["next [for <pool>]", "rotate: show the next key (masked)"],
|
|
69
|
+
["consume <n> [on <pool>]", "record token usage"],
|
|
70
|
+
["serve", "start the web UI + API server"],
|
|
71
|
+
["clear", "clear the screen"],
|
|
72
|
+
["exit", "quit"],
|
|
73
|
+
];
|
|
74
|
+
const w = Math.max(...rows.map(([c]) => c.length));
|
|
75
|
+
return ["Commands:", ...rows.map(([c, d]) => ` ${c.padEnd(w)} ${d}`)].join("\n");
|
|
76
|
+
}
|
|
77
|
+
export function maskTip() {
|
|
78
|
+
return "Keys are shown masked (gsk_…ab) — raw values never print.";
|
|
79
|
+
}
|
|
80
|
+
// Re-export box helper for tests (kept trivial on purpose).
|
|
81
|
+
export { box };
|