mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
* fix(muse): make launch defaults usable and configure each agent Disable sandboxing and trust workspaces by default so package installs and project rules work. Replace the host settings UI with validated per-agent providerOptions, preserving posture across session resume. * docs(muse): describe shared provider options configuration
217 lines
7.2 KiB
TypeScript
217 lines
7.2 KiB
TypeScript
import {
|
|
negotiateProviderCapabilities,
|
|
requireProviderCapabilities,
|
|
type ProviderConnection,
|
|
type ProviderEvent,
|
|
type ProviderInput,
|
|
type ProviderLaunch,
|
|
type ProviderRegistration,
|
|
type ProviderStatus,
|
|
} from "@getpaseo/plugin/server/provider";
|
|
import { serveArgs } from "./options.js";
|
|
import { Usage } from "./usage.js";
|
|
import { execFile } from "node:child_process";
|
|
import { Catalog, launchKey } from "./catalog.js";
|
|
import { MspConnection } from "./connection.js";
|
|
import { MuseError, actionableError } from "./errors.js";
|
|
import { Sessions } from "./sessions.js";
|
|
import { Session } from "./session.js";
|
|
import { accountSchema } from "./wire.js";
|
|
|
|
const capabilities = [
|
|
"prompt.message",
|
|
"prompt.command",
|
|
"prompt.image",
|
|
"prompt.steer",
|
|
"session.configure",
|
|
"session.persistence",
|
|
"permission",
|
|
"session.list",
|
|
] as const;
|
|
|
|
export function createMuseProvider(usage: Usage): ProviderRegistration {
|
|
return {
|
|
id: "muse",
|
|
label: "Muse Code",
|
|
icon: "icon.svg",
|
|
command: ["muse"],
|
|
async getCatalogCacheKey(options) {
|
|
return launchKey(requireLaunch(options.launch));
|
|
},
|
|
async status({ launch }) {
|
|
if (!launch)
|
|
return { available: false, diagnostic: "Install Muse Code and ensure `muse` is on PATH." };
|
|
usage.remember(launch);
|
|
return status(launch);
|
|
},
|
|
async connect(request) {
|
|
if (!request.versions.includes(1))
|
|
throw new MuseError("protocol", "Provider protocol version 1 is required");
|
|
usage.remember(requireLaunch(request.launch));
|
|
return connect(
|
|
requireLaunch(request.launch),
|
|
negotiateProviderCapabilities(request.capabilities, capabilities),
|
|
usage,
|
|
);
|
|
},
|
|
};
|
|
}
|
|
function requireLaunch(launch: ProviderLaunch | undefined): ProviderLaunch {
|
|
if (!launch) throw new MuseError("missingLaunch", "Muse requires a daemon-resolved executable");
|
|
return launch;
|
|
}
|
|
async function status(launch: ProviderLaunch): Promise<ProviderStatus> {
|
|
let host: MspConnection | undefined;
|
|
try {
|
|
const versionText = await new Promise<string>((resolve, reject) => {
|
|
execFile(
|
|
launch.command,
|
|
[...launch.args, "--version"],
|
|
{ env: launch.env, timeout: 3000, maxBuffer: 8192 },
|
|
(error, stdout) => {
|
|
if (error) reject(new MuseError("version", error.message));
|
|
else resolve(stdout);
|
|
},
|
|
);
|
|
});
|
|
const match = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(versionText);
|
|
if (!match) return { available: false, diagnostic: "Muse returned an unrecognized version." };
|
|
const major = Number(match[1]);
|
|
const minor = Number(match[2]);
|
|
if (major < 1 || (major === 1 && minor < 3))
|
|
return { available: false, diagnostic: `Update Muse Code: found ${match[0]}, need ≥1.3.0` };
|
|
host = new MspConnection({ launch, timeoutMs: 3000 });
|
|
await host.initialize();
|
|
const account = await host.request("account/read", {}, accountSchema);
|
|
if (account.state === "loggedOut")
|
|
return { available: false, diagnostic: "Run `muse login` or set META_API_KEY" };
|
|
return { available: true };
|
|
} catch (error) {
|
|
return { available: false, diagnostic: actionableError(error, launch).message };
|
|
} finally {
|
|
await host?.close();
|
|
}
|
|
}
|
|
function connect(
|
|
launch: ProviderLaunch,
|
|
negotiated: readonly string[],
|
|
usage: Usage,
|
|
): ProviderConnection {
|
|
const listeners = new Set<(event: ProviderEvent) => void>();
|
|
const sessions = new Map<string, Session>();
|
|
const catalog = new Catalog();
|
|
const imports = new Sessions();
|
|
let closed = false;
|
|
function emit(event: ProviderEvent): void {
|
|
if (!closed) for (const listener of listeners) listener(event);
|
|
}
|
|
async function dispatch(input: ProviderInput): Promise<void> {
|
|
if (input.type === "sessions") {
|
|
emit({
|
|
type: "sessions",
|
|
requestId: input.requestId,
|
|
sessions: await imports.list(launch, input),
|
|
});
|
|
return;
|
|
}
|
|
if (input.type === "catalog") {
|
|
emit({ type: "catalog", requestId: input.requestId, catalog: await catalog.read(launch) });
|
|
return;
|
|
}
|
|
if (input.type === "session.open") {
|
|
if (sessions.has(input.sessionId))
|
|
throw new MuseError("duplicateSession", "Muse session is already open");
|
|
const session = new Session({
|
|
id: input.sessionId,
|
|
config: input.config,
|
|
launch,
|
|
emit,
|
|
capabilities: negotiated,
|
|
serveArgs: serveArgs(input.config.providerOptions),
|
|
});
|
|
sessions.set(input.sessionId, session);
|
|
try {
|
|
await session.open(input);
|
|
usage.attach(
|
|
input.sessionId,
|
|
{ ...launch, env: { ...launch.env, ...input.config.env } },
|
|
() => session.readUsage(),
|
|
);
|
|
} catch (error) {
|
|
sessions.delete(input.sessionId);
|
|
await session.close();
|
|
throw error;
|
|
}
|
|
return;
|
|
}
|
|
if (!("sessionId" in input))
|
|
throw new MuseError("unsupported", `Muse does not support ${input.type}`);
|
|
const session = sessions.get(input.sessionId);
|
|
if (!session) throw new MuseError("unknownSession", "Muse session is not open");
|
|
switch (input.type) {
|
|
case "session.prompt":
|
|
await session.prompt(input.prompt);
|
|
return;
|
|
case "session.configure":
|
|
await session.configure(input.changes);
|
|
break;
|
|
case "session.permission":
|
|
await session.answer(input.permissionId, input.response);
|
|
return;
|
|
case "session.interrupt":
|
|
await session.interrupt();
|
|
break;
|
|
case "session.close":
|
|
await session.close();
|
|
sessions.delete(input.sessionId);
|
|
usage.detach(input.sessionId);
|
|
emit({ type: "session.closed", sessionId: input.sessionId });
|
|
break;
|
|
default:
|
|
throw new MuseError("unsupported", `Muse does not support ${input.type}`);
|
|
}
|
|
if ("requestId" in input) emit({ type: "request.completed", requestId: input.requestId });
|
|
}
|
|
function failed(input: ProviderInput, failure: unknown): void {
|
|
const error = actionableError(failure, launch);
|
|
if (input.type === "session.prompt")
|
|
emit({
|
|
type: "session.prompt_result",
|
|
sessionId: input.sessionId,
|
|
clientMessageId: input.prompt.clientMessageId,
|
|
result: { type: "failed", error },
|
|
});
|
|
else if ("requestId" in input)
|
|
emit({ type: "request.failed", requestId: input.requestId, error });
|
|
else if ("sessionId" in input)
|
|
emit({ type: "session.runtime_failed", sessionId: input.sessionId, error });
|
|
}
|
|
return {
|
|
version: 1,
|
|
capabilities: negotiated,
|
|
async send(input) {
|
|
if (closed) throw new MuseError("closed", "Muse connection is closed");
|
|
requireProviderCapabilities(negotiated, input);
|
|
queueMicrotask(() => {
|
|
if (!closed) void dispatch(input).catch((error: unknown) => failed(input, error));
|
|
});
|
|
},
|
|
onEvent(listener) {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
async close() {
|
|
if (closed) return;
|
|
closed = true;
|
|
await Promise.all([
|
|
catalog.close(),
|
|
imports.close(),
|
|
...[...sessions.values()].map((session) => session.close()),
|
|
]);
|
|
for (const id of sessions.keys()) usage.detach(id);
|
|
sessions.clear();
|
|
listeners.clear();
|
|
},
|
|
};
|
|
}
|