Files
paseo/plugins/muse-provider/server/usage.ts
T
Mohamed Boudra 73d25317a4 fix(usage): derive window names and unify pin selection (#5903)
* fix(usage): derive window names and unify pin selection

Quota slots do not identify a period. Derive numeric windows and IDs from reported duration and quota scope; keep unknown lengths neutral. Resolve source defaults and explicit pins through one model, with a persisted empty selection distinct from defaults.

* fix(plugins): declare the duration helper host requirement

* fix(usage): verify persisted selections at the storage boundary
2026-10-02 11:18:13 +01:00

116 lines
4.1 KiB
TypeScript

import { homedir } from "node:os";
import path from "node:path";
import { z } from "zod";
import type { UsageSourceRegistration } from "@getpaseo/plugin/server";
import type { ProviderLaunch } from "@getpaseo/plugin/server/provider";
import {
hashAccountKey,
unavailable,
windowFromUsedPct,
windowFromReportedDuration,
toneFromUsedPct,
} from "@getpaseo/plugin/server/usage";
import { MspConnection } from "./connection.js";
import { usageSchema } from "./wire.js";
const inputSchema = z.object({ account: z.string() }).strict();
type Observation = z.infer<typeof usageSchema>;
export class Usage {
private readonly launches = new Map<string, ProviderLaunch>();
private readonly sessions = new Map<
string,
{ account: string; read: () => Promise<Observation> }
>();
remember(launch: ProviderLaunch): { account: string } {
// MSP 1.4.1 has no stable account ID; swapping accounts in one config directory shares a report ID.
const configHome =
launch.env.XDG_CONFIG_HOME ?? path.join(launch.env.HOME ?? homedir(), ".config");
const key = hashAccountKey(path.resolve(configHome, "muse"));
this.launches.set(key, launch);
return { account: key };
}
attach(id: string, launch: ProviderLaunch, read: () => Promise<Observation>): void {
this.sessions.set(id, { ...this.remember(launch), read });
}
detach(id: string): void {
this.sessions.delete(id);
}
private async read(account: string, launch: ProviderLaunch): Promise<Observation> {
const readers = [...this.sessions.values()].filter((session) => session.account === account);
if (readers.length > 0) {
const observations = await Promise.all(readers.map((session) => session.read()));
const usages = observations.flatMap((observation) =>
observation.usage ? [observation.usage] : [],
);
usages.sort((a, b) => b.observedAtMs - a.observedAtMs);
return usages.length > 0 ? { usage: usages[0] } : {};
}
const host = new MspConnection({ launch });
try {
await host.initialize();
return await host.request("usage/read", {}, usageSchema);
} finally {
await host.close();
}
}
registration(): UsageSourceRegistration {
return {
id: "muse",
label: "Muse Code",
icon: "icon.svg",
input: inputSchema,
discover: async () =>
[...this.launches.keys()].map((account) => ({
key: account,
label: "Muse Code",
input: { account },
})),
fetch: async (input) => {
const { account } = inputSchema.parse(input);
const launch = this.launches.get(account);
if (!launch) throw new Error("Muse login no longer exists");
try {
const { usage } = await this.read(account, launch);
if (!usage) return unavailable({ kind: "no_quota", detail: "No usage quota reported" });
return {
status: "available",
planLabel: usage.tier,
windows: [
windowFromReportedDuration({
durationSeconds: usage.window.windowDurationMins * 60,
unknown: { id: "window", label: "Current window", shortLabel: "" },
summary: true,
utilizationPct: usage.window.usedPercent,
resetsAt: new Date(usage.window.resetsAtMs).toISOString(),
tone: toneFromUsedPct(usage.window.usedPercent),
}),
windowFromUsedPct({
id: "weekly",
label: "Weekly",
shortLabel: "wk",
summary: true,
utilizationPct: usage.weekly.usedPercent,
resetsAt: new Date(usage.weekly.resetsAtMs).toISOString(),
tone: toneFromUsedPct(usage.weekly.usedPercent),
}),
],
details: [
{
id: "observed",
label: "Observed",
value: new Date(usage.observedAtMs).toISOString(),
},
],
};
} catch (error) {
return {
status: "error",
error: error instanceof Error ? error.message : String(error),
};
}
},
};
}
}