1import { ServerProviderModel } from "@t3tools/contracts";
2import { MuseSettings } from "../settings.ts";
3import { resolveSpawnCommand } from "@t3tools/shared/shell";
4import * as DateTime from "effect/DateTime";
5import * as Effect from "effect/Effect";
6import * as Option from "effect/Option";
7import * as Result from "effect/Result";
8import * as ChildProcess from "effect/process/ChildProcess";
9
10import { createMuseSdkHost, makeMuseEnvironment } from "./sdk.ts";
11import { parseMuseVersion } from "./maintenance.ts";
12import { discoverMuseModels, museModelCapabilities } from "./modelCatalog.ts";
13import {
14 buildServerProvider,
15 COMPACT_SLASH_COMMAND,
16 DEFAULT_TIMEOUT_MS,
17 isCommandMissingCause,
18 providerModelsFromSettings,
19 spawnAndCollect,
20 type ProviderProbeResult,
21} from "@t3tools/provider-core/server/snapshotProbe";
22
23const MUSE_PRESENTATION = {
24 displayName: "Muse Code",
25 showInteractionModeToggle: false,
26 reportsContextWindow: true,
27 // Muse has no native "accept edits" mode, so T3 offers only the two it maps
28 // directly: :ask-me with promptUnmatched, and :unrestricted with allowAll.
29 supportedRuntimeModes: ["approval-required", "full-access"],
30} as const;
31
32const FALLBACK_CAPABILITIES = museModelCapabilities();
33
34export const makePendingMuseProvider = Effect.fn("makePendingMuseProvider")(function* (
35 settings: MuseSettings,
36) {
37 return buildServerProvider({
38 presentation: MUSE_PRESENTATION,
39 enabled: settings.enabled,
40 checkedAt: DateTime.formatIso(yield* DateTime.now),
41 models: providerModelsFromSettings([], settings.customModels, FALLBACK_CAPABILITIES),
42 slashCommands: settings.enabled ? [COMPACT_SLASH_COMMAND] : [],
43 probe: {
44 installed: false,
45 version: null,
46 status: "warning",
47 auth: { status: "unknown" },
48 message: settings.enabled
49 ? "Checking Muse Code CLI availability..."
50 : "Muse Code is disabled in T3 Code settings.",
51 },
52 });
53});
54
55export const checkMuseProviderStatus = Effect.fn("checkMuseProviderStatus")(function* (
56 settings: MuseSettings,
57 environment?: NodeJS.ProcessEnv,
58 cwd?: string,
59 createHost: typeof createMuseSdkHost = createMuseSdkHost,
60) {
61 if (!settings.enabled) return yield* makePendingMuseProvider(settings);
62 const checkedAt = DateTime.formatIso(yield* DateTime.now);
63 // The driver passes a prepared environment; direct callers get the host's without META_API_KEY.
64 const museEnvironment = environment ?? makeMuseEnvironment();
65 const snapshot = (probe: ProviderProbeResult, models: ReadonlyArray<ServerProviderModel> = []) =>
66 buildServerProvider({
67 presentation: MUSE_PRESENTATION,
68 enabled: true,
69 checkedAt,
70 models: providerModelsFromSettings(models, settings.customModels, FALLBACK_CAPABILITIES),
71 slashCommands: [COMPACT_SLASH_COMMAND],
72 probe,
73 });
74 const versionResult = yield* Effect.gen(function* () {
75 const spawnCommand = yield* resolveSpawnCommand(settings.binaryPath, ["--version"], {
76 env: museEnvironment,
77 });
78 return yield* spawnAndCollect(
79 settings.binaryPath,
80 ChildProcess.make(spawnCommand.command, spawnCommand.args, {
81 env: museEnvironment,
82 shell: spawnCommand.shell,
83 ...(cwd ? { cwd } : {}),
84 }),
85 );
86 }).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
87
88 if (Result.isFailure(versionResult)) {
89 const missing = isCommandMissingCause(versionResult.failure);
90 return snapshot({
91 installed: !missing,
92 version: null,
93 status: "error",
94 auth: { status: "unknown" },
95 message: missing
96 ? "Muse Code CLI (`muse`) was not found. Install Muse Code and run `muse login` on this T3 server host."
97 : "Failed to execute Muse Code CLI. Check its binary path on this T3 server host.",
98 });
99 }
100 if (Option.isNone(versionResult.success)) {
101 return snapshot({
102 installed: true,
103 version: null,
104 status: "error",
105 auth: { status: "unknown" },
106 message: "Muse Code CLI version check timed out.",
107 });
108 }
109 const versionOutput = versionResult.success.value;
110 const version = parseMuseVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`);
111 if (versionOutput.code !== 0) {
112 return snapshot({
113 installed: true,
114 version,
115 status: "error",
116 auth: { status: "unknown" },
117 message: "Muse Code CLI is installed but failed to run.",
118 });
119 }
120
121 const catalog = yield* discoverMuseModels(settings, museEnvironment, cwd, createHost).pipe(
122 Effect.scoped,
123 Effect.timeoutOption(12_000),
124 Effect.result,
125 );
126 if (Result.isFailure(catalog) || Option.isNone(catalog.success)) {
127 return snapshot({
128 installed: true,
129 version,
130 status: "error",
131 auth: { status: "unknown" },
132 message:
133 "Muse Code SDK could not read the model catalog. Check your Muse installation and run `muse login` on this T3 server host.",
134 });
135 }
136 const models = catalog.success.value;
137 // MSP has no auth signal, and model/list answers from Muse's cache even when logged out.
138 // Auth failures surface on the first turn instead.
139 return snapshot(
140 {
141 installed: true,
142 version,
143 auth: { status: "unknown" },
144 ...(models.length > 0
145 ? { status: "ready" }
146 : {
147 status: "warning",
148 message:
149 "Muse Code returned no models. Run `muse login` on this T3 server host and refresh its status.",
150 }),
151 },
152 models,
153 );
154});