quotacap 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +78 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.js +33 -0
- package/dist/adapters/manual.d.ts +8 -0
- package/dist/adapters/manual.js +25 -0
- package/dist/adapters/types.d.ts +17 -0
- package/dist/adapters/types.js +1 -0
- package/dist/advisory/engine.d.ts +11 -0
- package/dist/advisory/engine.js +26 -0
- package/dist/advisory/types.d.ts +10 -0
- package/dist/advisory/types.js +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +86 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +32 -0
- package/dist/daemon.d.ts +22 -0
- package/dist/daemon.js +30 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +142 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +101 -0
- package/dist/src/adapters/claude.d.ts +7 -0
- package/dist/src/adapters/claude.js +78 -0
- package/dist/src/adapters/index.d.ts +18 -0
- package/dist/src/adapters/index.js +33 -0
- package/dist/src/adapters/manual.d.ts +8 -0
- package/dist/src/adapters/manual.js +25 -0
- package/dist/src/adapters/types.d.ts +17 -0
- package/dist/src/adapters/types.js +1 -0
- package/dist/src/advisory/engine.d.ts +11 -0
- package/dist/src/advisory/engine.js +26 -0
- package/dist/src/advisory/types.d.ts +10 -0
- package/dist/src/advisory/types.js +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +86 -0
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +32 -0
- package/dist/src/daemon.d.ts +22 -0
- package/dist/src/daemon.js +30 -0
- package/dist/src/http/server.d.ts +4 -0
- package/dist/src/http/server.js +142 -0
- package/dist/src/mcp/server.d.ts +41 -0
- package/dist/src/mcp/server.js +101 -0
- package/dist/src/store/db.d.ts +2 -0
- package/dist/src/store/db.js +18 -0
- package/dist/src/store/quotas.d.ts +5 -0
- package/dist/src/store/quotas.js +28 -0
- package/dist/src/webAssets.d.ts +1 -0
- package/dist/src/webAssets.js +2 -0
- package/dist/src/webHtml.d.ts +1 -0
- package/dist/src/webHtml.js +2 -0
- package/dist/store/db.d.ts +2 -0
- package/dist/store/db.js +18 -0
- package/dist/store/quotas.d.ts +5 -0
- package/dist/store/quotas.js +28 -0
- package/dist/tests/adapters/claude.test.d.ts +1 -0
- package/dist/tests/adapters/claude.test.js +42 -0
- package/dist/tests/adapters/manual.test.d.ts +1 -0
- package/dist/tests/adapters/manual.test.js +10 -0
- package/dist/tests/advisory/engine.test.d.ts +1 -0
- package/dist/tests/advisory/engine.test.js +11 -0
- package/dist/tests/bootstrap.test.d.ts +1 -0
- package/dist/tests/bootstrap.test.js +14 -0
- package/dist/tests/cli/cli.test.d.ts +1 -0
- package/dist/tests/cli/cli.test.js +10 -0
- package/dist/tests/http/api.test.d.ts +1 -0
- package/dist/tests/http/api.test.js +14 -0
- package/dist/tests/integration.test.d.ts +1 -0
- package/dist/tests/integration.test.js +48 -0
- package/dist/tests/mcp/server.test.d.ts +1 -0
- package/dist/tests/mcp/server.test.js +7 -0
- package/dist/tests/store/db.test.d.ts +1 -0
- package/dist/tests/store/db.test.js +13 -0
- package/dist/tests/web/build.test.d.ts +1 -0
- package/dist/tests/web/build.test.js +8 -0
- package/dist/web/src/App.d.ts +3 -0
- package/dist/web/src/App.js +78 -0
- package/dist/web/src/api.d.ts +2 -0
- package/dist/web/src/api.js +8 -0
- package/dist/webAssets.d.ts +1 -0
- package/dist/webAssets.js +2 -0
- package/dist/webHtml.d.ts +1 -0
- package/dist/webHtml.js +2 -0
- package/package.json +44 -0
- package/web/dist/assets/index-G2N84rBQ.js +40 -0
- package/web/dist/index.html +2 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export function getConfigPath(p) {
|
|
6
|
+
if (p)
|
|
7
|
+
return p;
|
|
8
|
+
return path.join(os.homedir(), ".quotacap", "config.json");
|
|
9
|
+
}
|
|
10
|
+
export function getDbPath(p) {
|
|
11
|
+
if (p)
|
|
12
|
+
return p;
|
|
13
|
+
return path.join(os.homedir(), ".quotacap", "quotacap.db");
|
|
14
|
+
}
|
|
15
|
+
const ConfigSchema = z.object({
|
|
16
|
+
port: z.number().default(8787),
|
|
17
|
+
pollMinutes: z.number().default(15),
|
|
18
|
+
enabledProviders: z.array(z.string()).default(["claude"]),
|
|
19
|
+
});
|
|
20
|
+
export async function readConfig(p) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(getConfigPath(p), "utf8");
|
|
23
|
+
return ConfigSchema.parse(JSON.parse(raw));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return ConfigSchema.parse({});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function writeConfig(c, p) {
|
|
30
|
+
await fs.mkdir(path.dirname(getConfigPath(p)), { recursive: true });
|
|
31
|
+
await fs.writeFile(getConfigPath(p), JSON.stringify(c, null, 2));
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function pollOnce(db: any, enabled: string[]): Promise<({
|
|
2
|
+
provider: string;
|
|
3
|
+
status: "fulfilled";
|
|
4
|
+
value: import("./adapters/types.js").Quota;
|
|
5
|
+
reason?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
provider: string;
|
|
8
|
+
status: "skipped";
|
|
9
|
+
reason: any;
|
|
10
|
+
value?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
provider: string;
|
|
13
|
+
status: "rejected";
|
|
14
|
+
reason: any;
|
|
15
|
+
value?: undefined;
|
|
16
|
+
})[]>;
|
|
17
|
+
export declare function startDaemon(): Promise<{
|
|
18
|
+
db: any;
|
|
19
|
+
timer: NodeJS.Timeout;
|
|
20
|
+
stop: () => void;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const start: typeof startDaemon;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { pollAll } from "./adapters/index.js";
|
|
2
|
+
import { upsertQuota } from "./store/quotas.js";
|
|
3
|
+
export async function pollOnce(db, enabled) {
|
|
4
|
+
const results = await pollAll(enabled);
|
|
5
|
+
for (const r of results) {
|
|
6
|
+
if (r.status === "fulfilled" && r.value)
|
|
7
|
+
upsertQuota(db, r.value);
|
|
8
|
+
else if (r.status === "rejected")
|
|
9
|
+
console.warn(`[quotacap] poll ${r.provider} failed: ${String(r.reason?.message ?? r.reason)}`);
|
|
10
|
+
// skipped (e.g. manual) is not a failure — no warning, not degraded
|
|
11
|
+
}
|
|
12
|
+
return results;
|
|
13
|
+
}
|
|
14
|
+
export async function startDaemon() {
|
|
15
|
+
const { getDbPath, readConfig } = await import("./config.js");
|
|
16
|
+
const { openDb, migrate } = await import("./store/db.js");
|
|
17
|
+
const db = openDb(getDbPath());
|
|
18
|
+
migrate(db);
|
|
19
|
+
const cfg = await readConfig();
|
|
20
|
+
const intervalMs = (cfg.pollMinutes ?? 15) * 60 * 1000;
|
|
21
|
+
// initial poll (fire-and-forget, don't block start)
|
|
22
|
+
pollOnce(db, cfg.enabledProviders).catch(e => console.warn("[quotacap] initial poll failed", e?.message ?? String(e)));
|
|
23
|
+
const jitter = Math.floor(Math.random() * 5000);
|
|
24
|
+
const timer = setInterval(() => { pollOnce(db, cfg.enabledProviders).catch(e => console.warn("[quotacap] interval poll failed", e?.message ?? String(e))); }, intervalMs + jitter);
|
|
25
|
+
// keep event loop alive — this is the only long-lived process per design
|
|
26
|
+
// (previous unref() caused immediate exit after the initial poll)
|
|
27
|
+
return { db, timer, stop: () => clearInterval(timer) };
|
|
28
|
+
}
|
|
29
|
+
// alias for plan's daemon.start() naming
|
|
30
|
+
export const start = startDaemon;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { webHtml } from "../webHtml.js";
|
|
6
|
+
import { webAssets } from "../webAssets.js";
|
|
7
|
+
// per-app state registry for backward-compat helpers
|
|
8
|
+
const appStates = new WeakMap();
|
|
9
|
+
const allStates = new Set();
|
|
10
|
+
export function buildApp(db) {
|
|
11
|
+
const state = { lastPollAt: null, lastRefreshAt: 0, lastRefreshResult: null };
|
|
12
|
+
const app = Fastify({ logger: false });
|
|
13
|
+
appStates.set(app, state);
|
|
14
|
+
allStates.add(state);
|
|
15
|
+
app.addHook("onClose", async () => {
|
|
16
|
+
appStates.delete(app);
|
|
17
|
+
allStates.delete(state);
|
|
18
|
+
});
|
|
19
|
+
// expose for per-app helpers
|
|
20
|
+
app._quotacapState = state;
|
|
21
|
+
app.get("/health", async () => ({ ok: true, uptime: process.uptime(), lastPollAt: state.lastPollAt }));
|
|
22
|
+
app.get("/api/quotas", async () => {
|
|
23
|
+
const { getAllLatest } = await import("../store/quotas.js");
|
|
24
|
+
const quotas = getAllLatest(db);
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
return quotas.map((q) => {
|
|
27
|
+
const fetched = q.fetchedAt ? new Date(q.fetchedAt).getTime() : 0;
|
|
28
|
+
const ageMs = fetched ? now - fetched : Infinity;
|
|
29
|
+
const stale = ageMs > 60 * 60 * 1000;
|
|
30
|
+
return { ...q, stale, ageMs };
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
app.get("/api/recommendation", async (req) => {
|
|
34
|
+
const { getAllLatest } = await import("../store/quotas.js");
|
|
35
|
+
const quotas = getAllLatest(db);
|
|
36
|
+
if (!quotas.length) {
|
|
37
|
+
return { use: "none", reason: "no quotas yet", alternatives: [], advisories: [] };
|
|
38
|
+
}
|
|
39
|
+
let recommend;
|
|
40
|
+
try {
|
|
41
|
+
recommend = (await import("../advisory/engine.js")).recommend;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { use: quotas[0]?.provider ?? "none", reason: "advisory not yet implemented", alternatives: quotas };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const task = req.query?.task ?? "any";
|
|
48
|
+
return recommend(quotas, task);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return { use: quotas[0]?.provider ?? "none", reason: `advisory error: ${e?.message ?? String(e)}`, alternatives: quotas };
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
app.post("/api/refresh", async () => {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (now - state.lastRefreshAt < 60_000 && state.lastRefreshResult) {
|
|
57
|
+
return state.lastRefreshResult;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { pollOnce } = await import("../daemon.js");
|
|
61
|
+
const { readConfig } = await import("../config.js");
|
|
62
|
+
const cfg = await readConfig();
|
|
63
|
+
const results = await pollOnce(db, cfg.enabledProviders);
|
|
64
|
+
const fulfilled = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
65
|
+
const rejected = results.filter((r) => r.status === "rejected").map((r) => ({ provider: r.provider, reason: String(r.reason?.message ?? r.reason) }));
|
|
66
|
+
state.lastPollAt = new Date().toISOString();
|
|
67
|
+
state.lastRefreshAt = now;
|
|
68
|
+
state.lastRefreshResult = { fulfilled, rejected, lastPollAt: state.lastPollAt, results, degraded: rejected.length > 0 };
|
|
69
|
+
return state.lastRefreshResult;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return { fulfilled: [], rejected: [{ provider: "all", reason: String(e?.message ?? e) }], lastPollAt: state.lastPollAt, degraded: true, error: String(e?.message ?? e) };
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// serve built vite assets at /assets/* (web/dist/assets/*)
|
|
76
|
+
app.get("/assets/*", async (req, reply) => {
|
|
77
|
+
const assetPath = req.params["*"] ?? "";
|
|
78
|
+
// guard path traversal
|
|
79
|
+
if (assetPath.includes(".."))
|
|
80
|
+
return reply.status(400).send("bad path");
|
|
81
|
+
const candidates = [
|
|
82
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "../../web/dist/assets", assetPath),
|
|
83
|
+
path.join(process.cwd(), "web/dist/assets", assetPath),
|
|
84
|
+
];
|
|
85
|
+
for (const p of candidates) {
|
|
86
|
+
try {
|
|
87
|
+
const data = fs.readFileSync(p);
|
|
88
|
+
const ext = path.extname(p);
|
|
89
|
+
const type = ext === ".js" ? "application/javascript" : ext === ".css" ? "text/css" : ext === ".map" ? "application/json" : "application/octet-stream";
|
|
90
|
+
return reply.type(type).send(data);
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
}
|
|
94
|
+
const embedded = webAssets[assetPath];
|
|
95
|
+
if (embedded !== undefined) {
|
|
96
|
+
const ext = path.extname(assetPath);
|
|
97
|
+
const type = ext === ".js" ? "application/javascript" : ext === ".css" ? "text/css" : ext === ".map" ? "application/json" : "application/octet-stream";
|
|
98
|
+
return reply.type(type).send(embedded);
|
|
99
|
+
}
|
|
100
|
+
return reply.status(404).send("not found");
|
|
101
|
+
});
|
|
102
|
+
app.get("/", async (_req, reply) => {
|
|
103
|
+
const candidates = [
|
|
104
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "../../web/dist/index.html"),
|
|
105
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "../../web/index.html"),
|
|
106
|
+
path.join(process.cwd(), "web/dist/index.html"),
|
|
107
|
+
path.join(process.cwd(), "web/index.html"),
|
|
108
|
+
];
|
|
109
|
+
for (const p of candidates) {
|
|
110
|
+
try {
|
|
111
|
+
const html = fs.readFileSync(p, "utf8");
|
|
112
|
+
return reply.type("text/html").send(html);
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
}
|
|
116
|
+
return reply.type("text/html").send(webHtml ?? `<!doctype html><title>QuotaCap</title><div id=app>loading…</div>`);
|
|
117
|
+
});
|
|
118
|
+
return app;
|
|
119
|
+
}
|
|
120
|
+
export function getLastPollAt(app) {
|
|
121
|
+
if (app && appStates.has(app))
|
|
122
|
+
return appStates.get(app).lastPollAt;
|
|
123
|
+
// fallback: most recent state (for tests without app arg)
|
|
124
|
+
let last = null;
|
|
125
|
+
for (const s of allStates)
|
|
126
|
+
last = s.lastPollAt;
|
|
127
|
+
return last;
|
|
128
|
+
}
|
|
129
|
+
export function resetRefreshState(app) {
|
|
130
|
+
if (app && appStates.has(app)) {
|
|
131
|
+
const s = appStates.get(app);
|
|
132
|
+
s.lastPollAt = null;
|
|
133
|
+
s.lastRefreshAt = 0;
|
|
134
|
+
s.lastRefreshResult = null;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const s of allStates) {
|
|
138
|
+
s.lastPollAt = null;
|
|
139
|
+
s.lastRefreshAt = 0;
|
|
140
|
+
s.lastRefreshResult = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const tools: ({
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
task?: undefined;
|
|
8
|
+
provider?: undefined;
|
|
9
|
+
};
|
|
10
|
+
required: never[];
|
|
11
|
+
};
|
|
12
|
+
} | {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: {
|
|
18
|
+
task: {
|
|
19
|
+
type: string;
|
|
20
|
+
enum: string[];
|
|
21
|
+
};
|
|
22
|
+
provider?: undefined;
|
|
23
|
+
};
|
|
24
|
+
required?: undefined;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: string;
|
|
31
|
+
properties: {
|
|
32
|
+
provider: {
|
|
33
|
+
type: string;
|
|
34
|
+
};
|
|
35
|
+
task?: undefined;
|
|
36
|
+
};
|
|
37
|
+
required: string[];
|
|
38
|
+
};
|
|
39
|
+
})[];
|
|
40
|
+
export declare function handleTool(name: string, args: any): Promise<any>;
|
|
41
|
+
export declare function runMcpServer(): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const tools = [
|
|
2
|
+
{ name: "get_quotas", description: "All quotas with resets and health", inputSchema: { type: "object", properties: {}, required: [] } },
|
|
3
|
+
{ name: "get_recommendation", description: "Which provider to use next", inputSchema: { type: "object", properties: { task: { type: "string", enum: ["any", "heavy", "light"] } } } },
|
|
4
|
+
{ name: "forecast", description: "Burn vs ideal + waste for a provider", inputSchema: { type: "object", properties: { provider: { type: "string" } }, required: ["provider"] } },
|
|
5
|
+
];
|
|
6
|
+
async function fetchJson(path) {
|
|
7
|
+
const base = process.env.QUOTACAP_URL ?? "http://localhost:8787";
|
|
8
|
+
try {
|
|
9
|
+
const r = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(5000) });
|
|
10
|
+
if (!r.ok)
|
|
11
|
+
throw new Error(`HTTP ${r.status}`);
|
|
12
|
+
return r.json();
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
// any transport failure (node or bun wording) is a daemon-down situation
|
|
16
|
+
throw new Error(`daemon not running, run quotacap web — ${e?.message ?? String(e)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function handleTool(name, args) {
|
|
20
|
+
if (name === "get_quotas")
|
|
21
|
+
return fetchJson("/api/quotas");
|
|
22
|
+
if (name === "get_recommendation")
|
|
23
|
+
return fetchJson(`/api/recommendation?task=${args?.task ?? "any"}`);
|
|
24
|
+
if (name === "forecast") {
|
|
25
|
+
if (!args?.provider)
|
|
26
|
+
throw new Error("provider required");
|
|
27
|
+
const [quotas, rec] = await Promise.all([fetchJson("/api/quotas"), fetchJson(`/api/recommendation`)]);
|
|
28
|
+
const q = quotas.find((x) => x.provider === args.provider);
|
|
29
|
+
if (!q)
|
|
30
|
+
throw new Error(`unknown provider ${args.provider}`);
|
|
31
|
+
return { quota: q, advisory: rec.advisories?.find((a) => a.provider === args.provider) };
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`unknown tool ${name}`);
|
|
34
|
+
}
|
|
35
|
+
export async function runMcpServer() {
|
|
36
|
+
// Minimal MCP JSON-RPC stdio server — handles initialize, tools/list, tools/call, ping
|
|
37
|
+
const readline = await import("node:readline");
|
|
38
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
39
|
+
const respond = (id, result) => {
|
|
40
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
41
|
+
};
|
|
42
|
+
const error = (id, code, message) => {
|
|
43
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
44
|
+
};
|
|
45
|
+
rl.on("line", async (line) => {
|
|
46
|
+
if (!line.trim())
|
|
47
|
+
return;
|
|
48
|
+
let msg;
|
|
49
|
+
try {
|
|
50
|
+
msg = JSON.parse(line);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const { id, method, params } = msg;
|
|
56
|
+
// notifications have no id — no response
|
|
57
|
+
const isNotification = id === undefined;
|
|
58
|
+
try {
|
|
59
|
+
if (method === "initialize") {
|
|
60
|
+
if (!isNotification)
|
|
61
|
+
respond(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "quotacap", version: "0.0.1" } });
|
|
62
|
+
}
|
|
63
|
+
else if (method === "notifications/initialized") {
|
|
64
|
+
// no-op
|
|
65
|
+
}
|
|
66
|
+
else if (method === "tools/list") {
|
|
67
|
+
if (!isNotification)
|
|
68
|
+
respond(id, { tools });
|
|
69
|
+
}
|
|
70
|
+
else if (method === "tools/call") {
|
|
71
|
+
const toolName = params?.name;
|
|
72
|
+
const toolArgs = params?.arguments ?? {};
|
|
73
|
+
try {
|
|
74
|
+
const result = await handleTool(toolName, toolArgs);
|
|
75
|
+
const content = [{ type: "text", text: JSON.stringify(result, null, 2) }];
|
|
76
|
+
if (!isNotification)
|
|
77
|
+
respond(id, { content });
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
const content = [{ type: "text", text: e?.message ?? String(e) }];
|
|
81
|
+
if (!isNotification)
|
|
82
|
+
respond(id, { content, isError: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (method === "ping") {
|
|
86
|
+
if (!isNotification)
|
|
87
|
+
respond(id, {});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (!isNotification)
|
|
91
|
+
error(id, -32601, `Method not found: ${method}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
if (!isNotification)
|
|
96
|
+
error(id, -32603, e?.message ?? String(e));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// keep alive
|
|
100
|
+
process.stdin.resume();
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// node:sqlite under node, bun:sqlite under bun — same sync API surface used here.
|
|
2
|
+
// Variable specifier keeps vite/rollup from statically resolving the other runtime's builtin.
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const BUN_SQLITE = "bun:sqlite";
|
|
6
|
+
let SQLite;
|
|
7
|
+
try {
|
|
8
|
+
SQLite = require("node:sqlite").DatabaseSync;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
SQLite = require(BUN_SQLITE).Database;
|
|
12
|
+
}
|
|
13
|
+
export function openDb(path) { return new SQLite(path); }
|
|
14
|
+
export function migrate(db) {
|
|
15
|
+
db.exec(`CREATE TABLE IF NOT EXISTS quotas(id INTEGER PRIMARY KEY, provider TEXT, plan TEXT, used_pct REAL, resets_at TEXT, period_start TEXT, raw TEXT, source TEXT, fetched_at TEXT);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS snapshots(day TEXT, provider TEXT, used_pct REAL, burn_rate REAL, ideal_rate REAL, PRIMARY KEY(day, provider));
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_quotas_provider ON quotas(provider);`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function upsertQuota(db: any, q: any): void;
|
|
2
|
+
export declare function getLatestByProvider(db: any, provider: string): any;
|
|
3
|
+
export declare function getAllLatest(db: any): any;
|
|
4
|
+
export declare const getQuotas: typeof getAllLatest;
|
|
5
|
+
export declare function getSnapshots(db: any): any;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function mapRow(row) {
|
|
2
|
+
if (!row)
|
|
3
|
+
return row;
|
|
4
|
+
// normalize snake_case DB row to camelCase Quota shape while keeping snake fields for compat
|
|
5
|
+
return {
|
|
6
|
+
...row,
|
|
7
|
+
usedPct: row.used_pct ?? row.usedPct,
|
|
8
|
+
resetsAt: row.resets_at ?? row.resetsAt,
|
|
9
|
+
periodStart: row.period_start ?? row.periodStart,
|
|
10
|
+
fetchedAt: row.fetched_at ?? row.fetchedAt,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function upsertQuota(db, q) {
|
|
14
|
+
db.prepare(`INSERT INTO quotas(provider, plan, used_pct, resets_at, period_start, raw, source, fetched_at) VALUES(?,?,?,?,?,?,?,?)`).run(q.provider, q.plan, q.usedPct, q.resetsAt, q.periodStart, q.raw, q.source, q.fetchedAt);
|
|
15
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
16
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES(?,?,?) ON CONFLICT(day, provider) DO UPDATE SET used_pct=excluded.used_pct`).run(day, q.provider, q.usedPct);
|
|
17
|
+
}
|
|
18
|
+
export function getLatestByProvider(db, provider) {
|
|
19
|
+
const row = db.prepare(`SELECT * FROM quotas WHERE provider=? ORDER BY fetched_at DESC LIMIT 1`).get(provider);
|
|
20
|
+
return mapRow(row);
|
|
21
|
+
}
|
|
22
|
+
export function getAllLatest(db) {
|
|
23
|
+
const rows = db.prepare(`SELECT * FROM quotas WHERE id IN (SELECT MAX(id) FROM quotas GROUP BY provider)`).all();
|
|
24
|
+
return rows.map(mapRow);
|
|
25
|
+
}
|
|
26
|
+
// alias for plan's getQuotas naming
|
|
27
|
+
export const getQuotas = getAllLatest;
|
|
28
|
+
export function getSnapshots(db) { return db.prepare(`SELECT * FROM snapshots ORDER BY day DESC`).all(); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const webAssets: Record<string, string>;
|