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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ICONS, DOCS_URL } from "./branding.js";
|
|
2
|
+
import { tokensCss } from "./tokens.js";
|
|
3
|
+
export const shellCss = `
|
|
4
|
+
html { scrollbar-gutter:stable; scrollbar-width:none; -ms-overflow-style:none; }
|
|
5
|
+
html::-webkit-scrollbar, body::-webkit-scrollbar { display:none; }
|
|
6
|
+
.shell { display:grid; grid-template-columns:var(--sbw,260px) 1fr; min-height:100vh; transition:grid-template-columns 250ms cubic-bezier(0.77,0,0.175,1); }
|
|
7
|
+
.shell.collapsed { --sbw:57px; }
|
|
8
|
+
.sidebar { background:var(--card); border-right:1px solid var(--line); display:flex; flex-direction:column; min-height:100vh; height:100vh; position:sticky; top:0; overflow:hidden; white-space:nowrap; }
|
|
9
|
+
.sb-header { height:58px; flex-shrink:0; display:flex; align-items:center; gap:4px; border-bottom:1px solid var(--line); padding:0 12px; overflow:hidden; }
|
|
10
|
+
.sb-acct { flex:1; min-width:0; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 12px; border-radius:8px; border:0; background:transparent; font-size:14px; }
|
|
11
|
+
.sb-acct span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:500; }
|
|
12
|
+
.sb-logo { width:40px; height:40px; flex-shrink:0; display:grid; place-items:center; overflow:hidden; }
|
|
13
|
+
.sb-logo img { width:36px; height:36px; object-fit:contain; }
|
|
14
|
+
.sb-nav { flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; padding:12px 11px 12px 14px; scrollbar-width:none; }
|
|
15
|
+
.sb-nav::-webkit-scrollbar { display:none; }
|
|
16
|
+
.mi { display:flex; align-items:center; gap:10px; width:100%; min-height:34px; padding:0 12px; border-radius:8px; border:0; background:transparent; font-size:14px; font-weight:500; color:var(--ink); cursor:pointer; text-decoration:none; text-align:left; }
|
|
17
|
+
.mi:hover { background:var(--paper-2); }
|
|
18
|
+
.mi.active { background:var(--line-soft); }
|
|
19
|
+
.mi .ic { opacity:.5; flex-shrink:0; width:16px; text-align:center; }
|
|
20
|
+
.mi .ic svg { display:block; width:18px; height:18px; }
|
|
21
|
+
.mi .ext { margin-left:auto; opacity:.45; display:inline-flex; flex-shrink:0; }
|
|
22
|
+
.sb-footer { height:48px; flex-shrink:0; display:flex; align-items:center; padding:0 14px; border-top:1px solid var(--line); position:sticky; bottom:0; background:var(--card); }
|
|
23
|
+
.collapse-btn { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; border:0; background:transparent; color:var(--ink-soft); cursor:pointer; }
|
|
24
|
+
.collapse-btn:hover { background:var(--paper-2); color:var(--ink); }
|
|
25
|
+
.shell.collapsed:not(.peeking) .lbl, .shell.collapsed:not(.peeking) .ext { display:none; }
|
|
26
|
+
.shell.collapsed:not(.peeking) .sb-header { padding:0 8px; justify-content:center; }
|
|
27
|
+
.shell.collapsed:not(.peeking) .sb-acct { display:none; }
|
|
28
|
+
.shell.collapsed:not(.peeking) .mi { justify-content:center; padding:0; }
|
|
29
|
+
.shell.peeking { --sbw:260px; }
|
|
30
|
+
.shell .lbl, .shell .sb-acct { opacity:1; transition:opacity 150ms ease; }
|
|
31
|
+
.shell.closing .lbl, .shell.closing .sb-acct { opacity:0; }
|
|
32
|
+
@keyframes pageIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:none; } }
|
|
33
|
+
.content { animation:pageIn .3s ease both; }
|
|
34
|
+
@media (max-width:720px) { .shell { --sbw:57px; } .lbl, .ext { display:none; } }`;
|
|
35
|
+
export function shellNav(active) {
|
|
36
|
+
const item = (href, icon, label) => {
|
|
37
|
+
const ext = href.startsWith("http");
|
|
38
|
+
return `<a class="mi${href === active ? " active" : ""}" href="${href}"${ext ? ' target="_blank" rel="noopener" title="Opens external link in a new tab"' : ""}><span class="ic">${icon}</span><span class="lbl">${label}</span>${ext ? `<span class="ext">${ICONS.external}</span>` : ""}</a>`;
|
|
39
|
+
};
|
|
40
|
+
return `<aside class="sidebar">
|
|
41
|
+
<div class="sb-header"><a class="sb-logo" aria-label="pool-anything home" href="/"><img src="/logo.png" alt="pool-anything" width="36" height="36" /></a><div class="sb-acct" title="Local account"><span>Local account</span></div></div>
|
|
42
|
+
<nav class="sb-nav">${item("/", ICONS.home, "Home")}${item("/pools", ICONS.pools, "Pools")}${item("/keys", ICONS.keys, "API key manager")}${item("/playground", ICONS.playground, "Playground")}${item("/analytics", ICONS.analytics, "Analytics")}${item(DOCS_URL, ICONS.docs, "Docs")}</nav>
|
|
43
|
+
<div class="sb-footer"><button class="collapse-btn" id="collapseBtn" type="button" data-sidebar="trigger" aria-expanded="true" aria-label="Collapse sidebar"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21.25 6.72v10.56a2.97 2.97 0 0 1-2.97 2.97H5.72a2.97 2.97 0 0 1-2.97-2.97V6.72a2.97 2.97 0 0 1 2.97-2.97h12.56a2.97 2.97 0 0 1 2.97 2.97"></path><path d="M6.25 7.25v9.5"></path></svg></button></div>
|
|
44
|
+
</aside><script>
|
|
45
|
+
(function () {
|
|
46
|
+
try {
|
|
47
|
+
var sh = document.getElementById('shell');
|
|
48
|
+
if (localStorage.getItem('sb-collapsed') !== '1') return;
|
|
49
|
+
sh.classList.add('collapsed');
|
|
50
|
+
var cb = document.getElementById('collapseBtn');
|
|
51
|
+
cb.setAttribute('aria-expanded', 'false');
|
|
52
|
+
cb.setAttribute('aria-label', 'Expand sidebar');
|
|
53
|
+
var sb = sh.querySelector('.sidebar');
|
|
54
|
+
if (sb && sb.matches(':hover')) sh.classList.add('peeking');
|
|
55
|
+
} catch (e) {}
|
|
56
|
+
})();
|
|
57
|
+
</script>`;
|
|
58
|
+
}
|
|
59
|
+
export const sharedHeader = `<header>
|
|
60
|
+
<div></div>
|
|
61
|
+
<div class="spacer">
|
|
62
|
+
<a href="${DOCS_URL}" target="_blank" rel="noopener">Docs</a>
|
|
63
|
+
</div>
|
|
64
|
+
</header>`;
|
|
65
|
+
/** Shared page layout matching Home exactly (container, cards, typography).
|
|
66
|
+
* Home tokens: --line/--subtle/--bg/--card, .wrap max-width 1400, card
|
|
67
|
+
* radius 16 + padding 18. Detail pages use compact top padding (32px, not
|
|
68
|
+
* Home's 16vh hero offset) so short content doesn't strand whitespace. */
|
|
69
|
+
export const sharedLayoutCss = `
|
|
70
|
+
${tokensCss}
|
|
71
|
+
.content { flex:1; min-width:0; display:flex; flex-direction:column; }
|
|
72
|
+
header { height:56px; background:var(--card); border-bottom:1px solid var(--line); display:flex; align-items:center; padding:0 16px; gap:8px; position:sticky; top:0; z-index:5; }
|
|
73
|
+
header .spacer { margin-left:auto; display:flex; gap:4px; }
|
|
74
|
+
header a { font-size:14px; padding:6px 12px; border-radius:8px; border:0; background:inherit; color:var(--ink); text-decoration:none; cursor:pointer; }
|
|
75
|
+
header a:hover { background:var(--paper-2); }
|
|
76
|
+
main { display:flex; flex-direction:column; align-items:center; justify-content:flex-start; padding:32px 16px 24px; }
|
|
77
|
+
.wrap { width:100%; max-width:1400px; margin:0 auto; display:flex; flex-direction:column; align-items:stretch; gap:16px; padding:0 8px; }
|
|
78
|
+
.page-head { display:flex; align-items:center; gap:10px; }
|
|
79
|
+
.page-head h1 { font-family:var(--font-serif); font-size:clamp(20px, 4vw, 26px); font-weight:500; margin:0; flex:1; }
|
|
80
|
+
.page-head img { width:28px; height:28px; object-fit:contain; }
|
|
81
|
+
a.back, a.crumb { font-size:13px; color:var(--ink-soft); text-decoration:none; }
|
|
82
|
+
a.back:hover, a.crumb:hover { color:var(--ink); text-decoration:underline; text-underline-offset:2px; }
|
|
83
|
+
.page-card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:18px; animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both; }
|
|
84
|
+
@keyframes fadeSlide { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:none; } }
|
|
85
|
+
.sec-label { font-size:11px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-soft); margin:0 0 8px; }
|
|
86
|
+
.support-grid { width:100%; display:grid; grid-template-columns:1fr 1fr; gap:16px; align-items:start; }
|
|
87
|
+
@media (max-width:900px) { .support-grid { grid-template-columns:1fr; } }
|
|
88
|
+
.support-card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:18px; display:flex; flex-direction:column; gap:8px; font-size:13px; }
|
|
89
|
+
.support-card .rel-row { display:flex; align-items:center; gap:10px; padding:8px 2px; border-top:1px solid var(--paper-2); font-size:13px; }
|
|
90
|
+
.support-card .rel-row:first-of-type { border-top:0; }
|
|
91
|
+
.support-card a { color:var(--ink); font-weight:500; text-decoration:none; }
|
|
92
|
+
.support-card a:hover { text-decoration:underline; text-underline-offset:2px; }
|
|
93
|
+
.support-card small { color:var(--ink-soft); font-weight:400; }
|
|
94
|
+
.support-card code { font-family:var(--font-mono); font-size:12px; background:var(--paper-2); border:1px solid var(--line); border-radius:8px; padding:8px 10px; overflow-x:auto; white-space:pre; display:block; }`;
|
|
95
|
+
export const shellJs = `<script>
|
|
96
|
+
const shell = document.getElementById('shell');
|
|
97
|
+
const collapseBtn = document.getElementById('collapseBtn');
|
|
98
|
+
try { if (localStorage.getItem('sb-collapsed') === '1') { shell.classList.add('collapsed'); collapseBtn.setAttribute('aria-expanded', 'false'); collapseBtn.setAttribute('aria-label', 'Expand sidebar'); } } catch {}
|
|
99
|
+
const saveSb = () => { try { localStorage.setItem('sb-collapsed', shell.classList.contains('collapsed') ? '1' : '0'); } catch {} };
|
|
100
|
+
const finishCollapse = (apply) => {
|
|
101
|
+
shell.classList.add('closing');
|
|
102
|
+
clearTimeout(finishCollapse.t);
|
|
103
|
+
finishCollapse.t = setTimeout(() => { apply(); shell.classList.remove('closing'); saveSb(); }, 160);
|
|
104
|
+
};
|
|
105
|
+
collapseBtn.addEventListener('click', () => {
|
|
106
|
+
if (shell.classList.contains('collapsed')) {
|
|
107
|
+
shell.classList.remove('collapsed'); shell.classList.remove('peeking');
|
|
108
|
+
collapseBtn.setAttribute('aria-expanded', 'true'); collapseBtn.setAttribute('aria-label', 'Collapse sidebar'); saveSb();
|
|
109
|
+
} else {
|
|
110
|
+
finishCollapse(() => {
|
|
111
|
+
shell.classList.add('collapsed'); shell.classList.remove('peeking');
|
|
112
|
+
collapseBtn.setAttribute('aria-expanded', 'false'); collapseBtn.setAttribute('aria-label', 'Expand sidebar');
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const sidebar = document.querySelector('.sidebar');
|
|
117
|
+
if (shell.classList.contains('collapsed') && sidebar.matches(':hover')) shell.classList.add('peeking');
|
|
118
|
+
let peekTimer;
|
|
119
|
+
sidebar.addEventListener('mouseenter', () => {
|
|
120
|
+
if (!shell.classList.contains('collapsed')) return;
|
|
121
|
+
clearTimeout(peekTimer); clearTimeout(finishCollapse.t); shell.classList.remove('closing');
|
|
122
|
+
peekTimer = setTimeout(() => shell.classList.add('peeking'), 150);
|
|
123
|
+
});
|
|
124
|
+
sidebar.addEventListener('mouseleave', () => {
|
|
125
|
+
clearTimeout(peekTimer);
|
|
126
|
+
if (!shell.classList.contains('peeking')) return;
|
|
127
|
+
finishCollapse(() => shell.classList.remove('peeking'));
|
|
128
|
+
});
|
|
129
|
+
</script>`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Canonical design tokens as a CSS `:root` block — docs-first source of truth.
|
|
2
|
+
* Values mirror design-tokens.json (verified by scripts/check-tokens.ts).
|
|
3
|
+
* Admin pages interpolate `tokensCss` instead of declaring their own `:root`.
|
|
4
|
+
* Custom code only — no framework theming layer involved. */
|
|
5
|
+
export const tokensCss = `
|
|
6
|
+
:root {
|
|
7
|
+
--paper:#FFFFFF; --paper-2:#F5F5F4; --card:#FFFFFF; --bg:#FFFFFF;
|
|
8
|
+
--ink:#1B1917; --ink-soft:#57534E; --ink-faint:#8A8680; --body-copy:#3F3B36;
|
|
9
|
+
--line:#E8E3D9; --line-soft:#EFEAE1; --active-pill:#E9E5DB;
|
|
10
|
+
--accent:#C15F3C; --accent-ink:#9A4A2E; --accent-soft:rgba(193,95,60,0.1);
|
|
11
|
+
--code-bg:#F5F5F4; --code-ink:#23201C; --code-dark:#1C1917; --code-dark-ink:#EDE6DA;
|
|
12
|
+
--green-bg:#EBF6EC; --green-line:#BFE3C4; --green-ink:#1E6B32; --green-chip:#22A355;
|
|
13
|
+
--info-chip:#E2DED4;
|
|
14
|
+
--warn-bg:#FFF3E4; --warn-line:#F0D9B5; --warn-ink:#9A5A00;
|
|
15
|
+
--danger-bg:#FDECEC; --danger-line:#F5C6C2; --danger-ink:#B3261E; --danger-ring:rgba(179,38,30,0.15);
|
|
16
|
+
--link-bg:#EAF1FE; --link-ink:#2456C6; --link-line:#C9DAFA;
|
|
17
|
+
--underline:#C9C2B4; --nav-hover:rgba(87,83,78,0.07);
|
|
18
|
+
--scrollbar:#D6D0C4; --chev:#A8A29E;
|
|
19
|
+
--tok-cmd:#B3401E; --tok-str:#0E6E3E; --tok-kw:#7A3DC0; --tok-com:#9B9489;
|
|
20
|
+
--chart-blue:#3b82f6; --chart-blue-fill:rgba(59,130,246,0.10);
|
|
21
|
+
--chart-red:#ef4444; --chart-red-fill:rgba(239,68,68,0.08);
|
|
22
|
+
--scrim:rgba(0,0,0,0.3); --overlay-strong:rgba(0,0,0,0.55);
|
|
23
|
+
--radius:8px; --radius-xs:5px; --radius-sm:6px; --radius-copy:7px;
|
|
24
|
+
--radius-md:8px; --radius-lg:10px; --radius-xl:12px; --radius-pill:999px;
|
|
25
|
+
--shadow-sm:0 1px 2px rgba(0,0,0,.15); --shadow-md:0 8px 24px rgba(0,0,0,.12);
|
|
26
|
+
--shadow-drawer:0 20px 60px rgba(0,0,0,.18);
|
|
27
|
+
--ctl-sm:26px; --ctl-icon:30px; --ctl-md:34px; --ctl-xl:40px;
|
|
28
|
+
--font-sans:-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
29
|
+
--font-serif:Georgia,"Times New Roman",serif;
|
|
30
|
+
--font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
31
|
+
color-scheme:light;
|
|
32
|
+
}`;
|
package/dist/cli/nl.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Natural-language command parser for the `pool-anything` REPL.
|
|
3
|
+
*
|
|
4
|
+
* Ceiling: keyword/regex matching only — no LLM, no grammar engine.
|
|
5
|
+
* If this outgrows ~20 intents, replace with a proper command table.
|
|
6
|
+
*/
|
|
7
|
+
function clean(input) {
|
|
8
|
+
return input
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/^please\s+/, "")
|
|
12
|
+
.replace(/[?.!]+$/, "")
|
|
13
|
+
.replace(/\s+/g, " ")
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
/** Strip filler words leaving the pool/provider reference. */
|
|
17
|
+
function refOf(s, drop) {
|
|
18
|
+
return s
|
|
19
|
+
.replace(drop, " ")
|
|
20
|
+
.replace(/\b(pool|pools|key|keys|provider|for|from|of|on|in|the|my|a|an|to|please)\b/g, " ")
|
|
21
|
+
.replace(/\s+/g, " ")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
const POOL_WORDS = /\b(list|ls|show|display|get|all|my)\b/g;
|
|
25
|
+
const USAGE_WORDS = /\b(show|display|get|list|what|whats|what's|check|my|pool|pools|usage|use|used|stats|statistics|status|tokens?|quota|remaining|consumption)\b/g;
|
|
26
|
+
const KEY_LIST_WORDS = /\b(list|ls|show|display|get|all|my|pool|pools|key|keys)\b/g;
|
|
27
|
+
const ADD_WORDS = /\b(add|import|paste|insert|append|put|save|store|key|keys|pool|pools|to|into|in|for|the|my|new)\b/g;
|
|
28
|
+
const WATCH_WORDS = /^watch\s*/;
|
|
29
|
+
const NEXT_WORDS = /^(next|rotate|pick|draw|get key|grab)\b\s*/;
|
|
30
|
+
const DELETE_WORDS = /\b(delete|remove|drop|destroy|pool|pools|the|my|a)\b/g;
|
|
31
|
+
export function parse(input) {
|
|
32
|
+
const raw = input.trim();
|
|
33
|
+
if (!raw)
|
|
34
|
+
return { kind: "unknown", raw: input };
|
|
35
|
+
const s = clean(input);
|
|
36
|
+
if (/^(exit|quit|:q|bye|goodbye|done)$/.test(s))
|
|
37
|
+
return { kind: "exit" };
|
|
38
|
+
if (/^(\.help|help|\?|commands)$/.test(s))
|
|
39
|
+
return { kind: "help" };
|
|
40
|
+
if (/^(clear|cls)$/.test(s))
|
|
41
|
+
return { kind: "clear" };
|
|
42
|
+
if (/^(version|--version|-v)$/.test(s))
|
|
43
|
+
return { kind: "version" };
|
|
44
|
+
if (/^(serve|start|run|launch)( the)?( server| ui| web| app)?$/.test(s))
|
|
45
|
+
return { kind: "serve" };
|
|
46
|
+
if (/^(server|web ui|dashboard)$/.test(s))
|
|
47
|
+
return { kind: "serve" };
|
|
48
|
+
// Keys before pools: "list keys ..." contains neither "pool" word necessarily.
|
|
49
|
+
if (/\bkeys?\b/.test(s) && /\b(list|ls|show|display|get)\b/.test(s)) {
|
|
50
|
+
return { kind: "list-keys", ref: refOf(s, KEY_LIST_WORDS) };
|
|
51
|
+
}
|
|
52
|
+
if (/\bproviders?\b/.test(s) && /\b(list|ls|show|display|get|all|what|available)\b/.test(s)) {
|
|
53
|
+
return { kind: "list-providers" };
|
|
54
|
+
}
|
|
55
|
+
if (/^(ls|pools?)$/.test(s) || (/\bpools?\b/.test(s) && /\b(list|ls|show|display|get|all|my)\b/.test(s))) {
|
|
56
|
+
return { kind: "list-pools" };
|
|
57
|
+
}
|
|
58
|
+
if (/^watch\b/.test(s)) {
|
|
59
|
+
return { kind: "watch", ref: refOf(s.replace(WATCH_WORDS, ""), POOL_WORDS) };
|
|
60
|
+
}
|
|
61
|
+
if (/^(next|rotate|pick|draw|grab)\b/.test(s)) {
|
|
62
|
+
return { kind: "next", ref: refOf(s.replace(NEXT_WORDS, ""), POOL_WORDS) };
|
|
63
|
+
}
|
|
64
|
+
const consume = s.match(/^(consume|record|log|bill|add usage|use)\b\s+(\d+)\b\s*(tokens?|toks?)?\s*(.*)$/);
|
|
65
|
+
if (consume) {
|
|
66
|
+
const tokens = Number(consume[2]);
|
|
67
|
+
const ref = refOf(consume[4] ?? "", /\b(on|for|from|to|in|tokens?|toks?|pool|pools|key|keys|the|my|usage)\b/g);
|
|
68
|
+
if (Number.isSafeInteger(tokens) && tokens > 0)
|
|
69
|
+
return { kind: "consume", tokens, ref };
|
|
70
|
+
}
|
|
71
|
+
if (/\b(add|import|paste|insert|append|put|save|store)\b/.test(s) && /\bkeys?\b/.test(s)) {
|
|
72
|
+
const count = s.match(/\b(\d+)\s*keys?\b/) ?? s.match(/\bkeys?\b.*?(\d+)\b/);
|
|
73
|
+
return {
|
|
74
|
+
kind: "add-keys",
|
|
75
|
+
ref: refOf(s, ADD_WORDS).replace(/^\d+\s*|\s*\d+$/, "").trim(),
|
|
76
|
+
count: count ? Number(count[1]) : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (/\b(delete|remove|drop|destroy)\b/.test(s) && /\bpools?\b/.test(s)) {
|
|
80
|
+
return { kind: "delete-pool", ref: refOf(s, DELETE_WORDS) };
|
|
81
|
+
}
|
|
82
|
+
if (/\b(create|new|make)\b/.test(s) && /\bpools?\b/.test(s)) {
|
|
83
|
+
// "create pool <name> for <provider>" — provider is the last word after "for".
|
|
84
|
+
// Name keeps the user's original casing (match on a case-preserved copy).
|
|
85
|
+
const cased = input.trim().replace(/\s+/g, " ");
|
|
86
|
+
const cm = cased.match(/\bfor\s+([A-Za-z0-9-]+)\s*$/) ?? cased.match(/\bprovider\s+([A-Za-z0-9-]+)\s*$/i);
|
|
87
|
+
const casedCut = cm ? cased.slice(0, cm.index).trim() : cased;
|
|
88
|
+
const name = casedCut
|
|
89
|
+
.replace(/\b(create|new|make|a|pool|pools|named|called|the|my)\b/gi, " ")
|
|
90
|
+
.replace(/\s+/g, " ")
|
|
91
|
+
.trim();
|
|
92
|
+
return { kind: "create-pool", name, provider: (cm?.[1] ?? "").toLowerCase() };
|
|
93
|
+
}
|
|
94
|
+
if (/\b(usage|use|used|stats|statistics|status|tokens?|toks?|quota|remaining|consumption)\b/.test(s)) {
|
|
95
|
+
return { kind: "usage", ref: refOf(s, USAGE_WORDS) };
|
|
96
|
+
}
|
|
97
|
+
return { kind: "unknown", raw: input };
|
|
98
|
+
}
|
|
99
|
+
// --- smart suggestions (edit distance over canonical forms + provider ids) ---
|
|
100
|
+
const CANONICAL = [
|
|
101
|
+
"list pools",
|
|
102
|
+
"list providers",
|
|
103
|
+
"list keys",
|
|
104
|
+
"show usage for",
|
|
105
|
+
"add keys to",
|
|
106
|
+
"create pool",
|
|
107
|
+
"delete pool",
|
|
108
|
+
"watch",
|
|
109
|
+
"serve",
|
|
110
|
+
"help",
|
|
111
|
+
"exit",
|
|
112
|
+
];
|
|
113
|
+
function distance(a, b) {
|
|
114
|
+
const m = a.length;
|
|
115
|
+
const n = b.length;
|
|
116
|
+
if (m === 0)
|
|
117
|
+
return n;
|
|
118
|
+
if (n === 0)
|
|
119
|
+
return m;
|
|
120
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
121
|
+
for (let i = 1; i <= m; i++) {
|
|
122
|
+
let cur = [i];
|
|
123
|
+
for (let j = 1; j <= n; j++) {
|
|
124
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
125
|
+
}
|
|
126
|
+
prev = cur;
|
|
127
|
+
}
|
|
128
|
+
return prev[n];
|
|
129
|
+
}
|
|
130
|
+
/** Best-guess suggestion for unknown input, or null. Compares against canonical commands. */
|
|
131
|
+
export function suggest(input, providerIds = []) {
|
|
132
|
+
const s = clean(input);
|
|
133
|
+
if (!s)
|
|
134
|
+
return null;
|
|
135
|
+
const first2 = s.split(" ").slice(0, 2).join(" ");
|
|
136
|
+
let best = null;
|
|
137
|
+
let bestD = Infinity;
|
|
138
|
+
for (const c of [...CANONICAL, ...providerIds.map((p) => `show usage for ${p}`)]) {
|
|
139
|
+
const target = c.startsWith("show usage") ? c : c;
|
|
140
|
+
const d = Math.min(distance(first2, target), distance(s, target) - Math.max(0, s.length - target.length) * 0.5);
|
|
141
|
+
if (d < bestD) {
|
|
142
|
+
bestD = d;
|
|
143
|
+
best = c;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return best !== null && bestD <= 4 ? best : null;
|
|
147
|
+
}
|