singularity-forge/src/cli-stats.ts
Mikael Hugo 980772cc90 refactor: migrate from better-sqlite3 to node:sqlite, npm glob to node:fs
Since Node >= 24 is the minimum engine, remove the better-sqlite3 fallback
chain from sf-db.ts, unit-ownership.ts, and cli-stats.ts. Use DatabaseSync
from node:sqlite directly. Also replace the `glob` npm package with built-in
node:fs/promises.glob and node:fs.globSync in pi-coding-agent LSP utils.

- Remove createRequire boilerplate and suppressSqliteWarning helper
- Simplify loadProvider() and openRawDb()
- Net -177 lines of fallback/middleware code

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-02 06:13:57 +02:00

259 lines
6.4 KiB
TypeScript

import { existsSync } from "node:fs";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
export interface ModelStatsRow {
model_id: string;
provider: string;
unit_type: string;
attempts: number;
successes: number;
avg_ms: number | null;
avg_cost: number | null;
avg_retries: number | null;
}
export interface QueryModelStatsOptions {
sinceSeconds: number;
unitType?: string;
}
interface SqliteStatement {
all(...params: unknown[]): Record<string, unknown>[];
}
interface SqliteDb {
prepare(sql: string): SqliteStatement;
close?: () => void;
}
interface ParsedStatsArgs {
command: "models";
sinceSeconds: number;
unitType?: string;
}
export function parseDurationSeconds(value: string): number {
const match = value.trim().match(/^(\d+(?:\.\d+)?)([smhdw])$/i);
if (!match) {
throw new Error(`Invalid duration: ${value}`);
}
const amount = Number(match[1]);
const unit = match[2].toLowerCase();
const multiplier =
unit === "s"
? 1
: unit === "m"
? 60
: unit === "h"
? 60 * 60
: unit === "d"
? 24 * 60 * 60
: 7 * 24 * 60 * 60;
return Math.max(1, Math.floor(amount * multiplier));
}
function toNumber(value: unknown, fallback = 0): number {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}
function toNullableNumber(value: unknown): number | null {
if (value === null || value === undefined) return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
export function queryModelStats(
db: SqliteDb,
options: QueryModelStatsOptions,
): ModelStatsRow[] {
const where = ["recorded_at > unixepoch() - ?"];
const params: unknown[] = [options.sinceSeconds];
if (options.unitType) {
where.push("unit_type = ?");
params.push(options.unitType);
}
const sql = `
SELECT model_id, provider, unit_type, COUNT(*) AS attempts,
SUM(succeeded) AS successes,
AVG(duration_ms) AS avg_ms,
AVG(cost_usd) AS avg_cost,
AVG(retries) AS avg_retries
FROM llm_task_outcomes
WHERE ${where.join(" AND ")}
GROUP BY model_id, unit_type
ORDER BY attempts DESC
`;
return db
.prepare(sql)
.all(...params)
.map((row) => ({
model_id: String(row.model_id ?? ""),
provider: String(row.provider ?? ""),
unit_type: String(row.unit_type ?? ""),
attempts: toNumber(row.attempts),
successes: toNumber(row.successes),
avg_ms: toNullableNumber(row.avg_ms),
avg_cost: toNullableNumber(row.avg_cost),
avg_retries: toNullableNumber(row.avg_retries),
}));
}
function formatSuccessRate(row: ModelStatsRow): string {
if (row.attempts <= 0) return "0.0%";
return `${((row.successes / row.attempts) * 100).toFixed(1)}%`;
}
function formatMs(value: number | null): string {
return value === null ? "n/a" : value.toFixed(0);
}
function formatCost(value: number | null): string {
return value === null ? "n/a" : `$${value.toFixed(4)}`;
}
function formatRetries(value: number | null): string {
return value === null ? "n/a" : value.toFixed(2);
}
function pad(value: string, width: number, align: "left" | "right"): string {
return align === "right" ? value.padStart(width) : value.padEnd(width);
}
export function formatModelStatsTable(rows: ModelStatsRow[]): string {
const headers = [
"model_id",
"provider",
"unit_type",
"attempts",
"success_rate",
"avg_ms",
"avg_cost",
"avg_retries",
];
const body = rows.map((row) => [
row.model_id,
row.provider,
row.unit_type,
String(row.attempts),
formatSuccessRate(row),
formatMs(row.avg_ms),
formatCost(row.avg_cost),
formatRetries(row.avg_retries),
]);
const widths = headers.map((header, index) =>
Math.max(header.length, ...body.map((row) => row[index].length)),
);
const numeric = new Set([3, 4, 5, 6, 7]);
const separator = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
const renderRow = (row: string[]) =>
`| ${row.map((cell, index) => pad(cell, widths[index], numeric.has(index) ? "right" : "left")).join(" | ")} |`;
return (
[
separator,
renderRow(headers),
separator,
...body.map(renderRow),
separator,
].join("\n") + "\n"
);
}
function parseStatsArgs(argv: string[]): ParsedStatsArgs | null {
const args = argv.slice(1);
if (args[0] !== "models") return null;
const parsed: ParsedStatsArgs = {
command: "models",
sinceSeconds: parseDurationSeconds("7d"),
};
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg === "--unit-type" && i + 1 < args.length) {
parsed.unitType = args[++i];
} else if (arg.startsWith("--unit-type=")) {
parsed.unitType = arg.slice("--unit-type=".length);
} else if (arg === "--since" && i + 1 < args.length) {
parsed.sinceSeconds = parseDurationSeconds(args[++i]);
} else if (arg.startsWith("--since=")) {
parsed.sinceSeconds = parseDurationSeconds(arg.slice("--since=".length));
}
}
return parsed;
}
function usage(): string {
return (
[
"Usage: sf stats models [--unit-type <type>] [--since <duration>]",
"",
"Examples:",
" sf stats models --since 7d",
" sf stats models --unit-type execute-task --since 24h",
].join("\n") + "\n"
);
}
function openSqliteDb(dbPath: string): SqliteDb {
return new DatabaseSync(dbPath, { readOnly: true }) as SqliteDb;
}
export async function runStatsCli(
argv: string[],
deps: {
basePath: string;
stdout?: Pick<typeof process.stdout, "write">;
stderr?: Pick<typeof process.stderr, "write">;
},
): Promise<number> {
const stdout = deps.stdout ?? process.stdout;
const stderr = deps.stderr ?? process.stderr;
let parsed: ParsedStatsArgs | null;
try {
parsed = parseStatsArgs(argv);
} catch (err) {
stderr.write(
`sf stats: ${err instanceof Error ? err.message : String(err)}\n`,
);
return 1;
}
if (!parsed) {
stderr.write(usage());
return 1;
}
const dbPath = join(deps.basePath, ".sf", "sf.db");
if (!existsSync(dbPath)) {
stderr.write(`sf stats: database not found at ${dbPath}\n`);
return 1;
}
let db: SqliteDb | null = null;
try {
db = openSqliteDb(dbPath);
const rows = queryModelStats(db, parsed);
if (rows.length === 0) {
stdout.write("No model outcomes recorded for the selected window.\n");
} else {
stdout.write(formatModelStatsTable(rows));
}
return 0;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (/no such table/i.test(message)) {
stdout.write("No model outcomes recorded yet.\n");
return 0;
}
stderr.write(`sf stats: ${message}\n`);
return 1;
} finally {
db?.close?.();
}
}