Files
paseo/plugins/muse-provider/server/catalog.ts
T
Mohamed Boudraandmillerben95 8332f4d1ff feat(muse): add Muse Code as a built-in provider plugin (#5719)
* test(muse): record real host protocol fixtures

* feat(muse): add built-in MSP provider

Co-authored-by: millerben95 <millerben95@users.noreply.github.com>

* fix(muse): expose provider settings in the app

The daemon registered host settings without a client contribution, leaving sandbox, network, and workspace trust unreachable. Share the definition with a settings screen and expose it through Command Center even when installed plugins are disabled.

* docs(muse): publish setup and provider support

* test: correct runtime fixtures and restart readiness

* fix(muse): hide reminder housekeeping from the timeline

* fix(muse): use the official Muse Code mark

Source: https://muse.ai/images/favicon/app-squiggle.svg

Preserve the official silhouette path verbatim. Remove its gradient definition and use currentColor, scaling the original 100-unit coordinates into the 24-unit viewBox. Black silhouette renders match pixel-for-pixel.

* fix(muse): adapt to current plugin APIs

Use the screen API introduced with sidebar plugin items. Usage reports now come from source account discovery; remove the retired agent usage-reference request and headline marker, and exercise discovery through the existing provider fixtures.

---------

Co-authored-by: millerben95 <millerben95@users.noreply.github.com>
2026-09-30 14:56:41 +01:00

92 lines
3.4 KiB
TypeScript

import type { ProviderCatalog, ProviderLaunch } from "@getpaseo/plugin/server/provider";
import type { z } from "zod";
import { createHash } from "node:crypto";
import { MspConnection } from "./connection.js";
import { catalogSchema, effortSchema } from "./wire.js";
export const modes = [
{ id: "onRequest", label: "Default", icon: "ShieldCheck", colorTier: "safe" },
{ id: "promptUnmatched", label: "Ask", icon: "ShieldAlert", colorTier: "moderate" },
{ id: "denyUnmatched", label: "Strict", icon: "ShieldCheck", colorTier: "safe" },
{
id: "allowAll",
label: "Full access",
icon: "ShieldOff",
colorTier: "dangerous",
isUnattended: true,
},
];
export const efforts = effortSchema.options.filter((effort) => effort !== "none");
export function launchKey(launch: ProviderLaunch): string {
// Hash values so credential-dependent cache identity never exposes credentials.
const routingKeys = Object.keys(launch.env)
.filter((key) => /^(META_|MUSE_|XDG_|HOME$|PATH$|HTTP_PROXY$|HTTPS_PROXY$|NO_PROXY$)/.test(key))
.sort();
const environment = routingKeys.map((key) => [key, launch.env[key]]);
return createHash("sha256")
.update(JSON.stringify([launch.command, launch.args, environment]))
.digest("hex");
}
export class Catalog {
private readonly cache = new Map<string, { expires: number; catalog: ProviderCatalog }>();
private readonly hosts = new Set<MspConnection>();
private closed = false;
async read(launch: ProviderLaunch): Promise<ProviderCatalog> {
if (this.closed) throw new Error("Muse catalogue is closed");
const key = launchKey(launch);
const cached = this.cache.get(key);
if (cached && cached.expires > Date.now()) return cached.catalog;
const host = new MspConnection({ launch, timeoutMs: 5000 });
this.hosts.add(host);
try {
await host.initialize();
const response = await host.request("model/list", {}, catalogSchema);
const catalog = presentCatalog(response);
if (!this.closed) {
if (this.cache.size >= 8) this.cache.delete(this.cache.keys().next().value!);
this.cache.set(key, { expires: Date.now() + 60000, catalog });
}
return catalog;
} finally {
await host.close();
this.hosts.delete(host);
}
}
async close(): Promise<void> {
this.closed = true;
this.cache.clear();
await Promise.all([...this.hosts].map((host) => host.close()));
}
}
export function presentCatalog(response: z.infer<typeof catalogSchema>): ProviderCatalog {
const models = response.models.map((model) => {
let supported: z.infer<typeof effortSchema>[] = efforts;
if (Array.isArray(model.reasoningEffortVariants) && model.reasoningEffortVariants.length > 0)
supported = model.reasoningEffortVariants;
else if (model.variants.length > 0) supported = model.variants;
return {
id: model.modelId,
label: model.displayLabel,
isDefault: model.isDefault,
contextWindowMaxTokens: model.contextLimit ?? undefined,
thinkingOptions: supported.map((id) => ({
id,
label: id,
isDefault: id === model.defaultReasoningEffort,
})),
defaultThinkingOptionId: model.defaultReasoningEffort ?? undefined,
metadata: { providerId: model.providerId },
};
});
const defaultModel = models.find((model) => model.isDefault);
return {
models,
modes,
defaultMode: "onRequest",
defaultModel: defaultModel?.id,
};
}