packages/provider-grok/src/server/status.ts
import {
type CustomModelSetting,
type ModelCapabilities,
type ServerProvider,
type ServerProviderAuth,
type ServerProviderModel,
type ServerProviderSlashCommand,
} from "@t3tools/contracts";
import type { GrokSettings } from "../settings.ts";
import type * as EffectAcpSchema from "effect-acp/compat";
import { causeErrorTag } from "@t3tools/shared/observability";
import * as Crypto from "effect/Crypto";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Option from "effect/Option";
import * as Result from "effect/Result";
import * as Schema from "effect/Schema";
import * as HttpClient from "effect/http/HttpClient";
import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import { createModelCapabilities } from "@t3tools/shared/model";
import { resolveSpawnCommand } from "@t3tools/shared/shell";
import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
import {
AUTH_PROBE_TIMEOUT_MS,
buildServerProvider,
COMPACT_SLASH_COMMAND,
isCommandMissingCause,
parseGenericCliVersion,
providerModelsFromSettings,
spawnAndCollect,
type ServerProviderDraft,
} from "@t3tools/provider-core/server/snapshotProbe";
import {
enrichProviderSnapshotWithVersionAdvisory,
type ProviderMaintenanceCapabilities,
} from "@t3tools/provider-core/server/maintenanceResolver";
import {
GROK_DEFAULT_MODEL_SLUG,
GROK_SUPPORTED_RUNTIME_MODES,
isValidGrokReasoningEffortToken,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
} from "./acpSupport.ts";
import { sessionModelStateFromInitialize } from "@t3tools/provider-acp/server/runtimeModel";
import { discoverGrokSkills } from "./skills.ts";
import { grokProbeEnvironment } from "./probeEnvironment.ts";
const GROK_PRESENTATION = {
displayName: "Grok",
supportsConversationRollback: false,
showInteractionModeToggle: false,
supportedRuntimeModes: GROK_SUPPORTED_RUNTIME_MODES,
} as const;
const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
});
const VERSION_PROBE_TIMEOUT_MS = 4_000;
// Grok writes the initialize result only after startup work (settings,
// skill discovery, file watching). From a home directory that work measured
// 9-14s on CLI 1.0.52, so an 8s cap records a healthy CLI as a failed probe.
const GROK_ACP_INITIALIZE_TIMEOUT_MS = 20_000;
const GROK_API_KEY_ENV = "XAI_API_KEY";
const GROK_BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{
slug: GROK_DEFAULT_MODEL_SLUG,
name: "Grok Build",
isCustom: false,
capabilities: EMPTY_CAPABILITIES,
},
];
export const buildInitialGrokProviderSnapshot = Effect.fn(
"GrokProviderStatus.buildInitialGrokProviderSnapshot",
)(function* (grokSettings: GrokSettings): Effect.fn.Return<ServerProviderDraft> {
const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
const models = grokModelsFromSettings(grokSettings.customModels);
if (!grokSettings.enabled) {
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: false,
checkedAt,
models,
probe: {
installed: false,
version: null,
status: "warning",
auth: { status: "unknown" },
message: "Grok is disabled in T3 Code settings.",
},
});
}
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: true,
checkedAt,
models,
probe: {
installed: true,
version: null,
status: "warning",
auth: { status: "unknown" },
message: "Checking Grok CLI availability...",
},
});
});
function grokModelsFromSettings(
customModels: ReadonlyArray<CustomModelSetting> | undefined,
builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS,
): ReadonlyArray<ServerProviderModel> {
return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES);
}
function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function grokReasoningOptionsFromModel(model: EffectAcpSchema.ModelInfo): {
readonly options: ReadonlyArray<{
value: string;
label: string;
description?: string;
isDefault?: boolean;
}>;
readonly currentValue: string | undefined;
} {
const meta = model._meta;
if (!meta || meta.supportsReasoningEffort === false) {
return { options: [], currentValue: undefined };
}
const currentEffort = nonEmptyString(meta.reasoningEffort);
const advertisedOptions = Array.isArray(meta.reasoningEfforts) ? meta.reasoningEfforts : [];
const seen = new Set<string>();
const options: Array<{
value: string;
label: string;
description?: string;
advertisedDefault: boolean;
}> = [];
for (const entry of advertisedOptions) {
if (!isRecord(entry)) {
continue;
}
const rawValue = nonEmptyString(entry.value);
const rawId = nonEmptyString(entry.id);
const value =
rawValue && isValidGrokReasoningEffortToken(rawValue)
? rawValue
: rawId && isValidGrokReasoningEffortToken(rawId)
? rawId
: undefined;
if (value === undefined || seen.has(value)) {
continue;
}
seen.add(value);
const description = nonEmptyString(entry.description);
options.push({
value,
label: nonEmptyString(entry.label) ?? value,
...(description ? { description } : {}),
advertisedDefault: entry.default === true || entry.isDefault === true,
});
}
const currentValue =
currentEffort && options.some((option) => option.value === currentEffort)
? currentEffort
: undefined;
const advertisedDefaults = options.filter((option) => option.advertisedDefault);
const selectedDefault =
advertisedDefaults.find((option) => option.value === currentValue)?.value ??
advertisedDefaults[0]?.value;
return {
options: options.map(({ value, label, description }) => ({
value,
label,
...(description ? { description } : {}),
...(value === selectedDefault ? { isDefault: true } : {}),
})),
currentValue: currentValue ?? selectedDefault,
};
}
export function buildGrokModelCapabilities(model: EffectAcpSchema.ModelInfo): ModelCapabilities {
const reasoning = grokReasoningOptionsFromModel(model);
return reasoning.options.length > 0
? createModelCapabilities({
optionDescriptors: [
{
id: "reasoningEffort",
label: "Reasoning",
type: "select",
options: reasoning.options.map((option) => ({
id: option.value,
label: option.label,
...(option.description ? { description: option.description } : {}),
...(option.isDefault ? { isDefault: true } : {}),
})),
...(reasoning.currentValue ? { currentValue: reasoning.currentValue } : {}),
},
],
})
: EMPTY_CAPABILITIES;
}
function buildGrokModelsFromSessionModelState(
modelState: EffectAcpSchema.SessionModelState | null | undefined,
): ReadonlyArray<ServerProviderModel> {
if (!modelState || modelState.availableModels.length === 0) {
return [];
}
const currentModelId = modelState.currentModelId.trim();
const seen = new Set<string>();
return modelState.availableModels.flatMap((model): ServerProviderModel[] => {
const slug = resolveGrokAcpBaseModelId(model.modelId);
if (!slug || seen.has(slug)) {
return [];
}
seen.add(slug);
return [
{
slug,
name: model.name.trim() || slug,
isCustom: false,
...(model.modelId.trim() === currentModelId ? { isDefault: true } : {}),
capabilities: buildGrokModelCapabilities(model),
},
];
});
}
export interface GrokModelsCliOutput {
readonly authenticated: boolean | null;
readonly models: ReadonlyArray<ServerProviderModel>;
}
/** Exit status is always 0; only output text indicates login state. */
export function parseGrokModelsCliOutput(output: string): GrokModelsCliOutput {
const authenticated = /you are logged in/i.test(output)
? true
: /not authenticated|not logged in/i.test(output)
? false
: null;
const seen = new Set<string>();
const models: ServerProviderModel[] = [];
for (const line of output.split(/\r?\n/)) {
const bullet = line.match(/^\s*[*-]\s+(\S+)(.*)$/);
if (!bullet?.[1]) {
continue;
}
const slug = resolveGrokAcpBaseModelId(bullet[1]);
if (seen.has(slug)) {
continue;
}
seen.add(slug);
models.push({
slug,
name: displayNameFromGrokModelSlug(slug),
isCustom: false,
...(/\(default\)/i.test(bullet[2] ?? "") ? { isDefault: true } : {}),
capabilities: EMPTY_CAPABILITIES,
});
}
return { authenticated, models };
}
function displayNameFromGrokModelSlug(slug: string): string {
return slug
.split(/[-_]/g)
.map((part) => (part.toLowerCase() === "grok" ? "Grok" : part))
.join(" ");
}
const runGrokCliCommand = Effect.fn("GrokProviderStatus.runGrokCliCommand")(function* (
grokSettings: GrokSettings,
args: ReadonlyArray<string>,
environment: NodeJS.ProcessEnv,
) {
const command = grokSettings.binaryPath || "grok";
const spawnCommand = yield* resolveSpawnCommand(command, args, { env: environment });
return yield* spawnAndCollect(
command,
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
env: environment,
shell: spawnCommand.shell,
}),
);
});
const probeGrokModels = Effect.fn("GrokProviderStatus.probeGrokModels")(function* (
grokSettings: GrokSettings,
environment: NodeJS.ProcessEnv,
) {
const modelsResult = yield* runGrokCliCommand(grokSettings, ["models"], environment).pipe(
Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS),
Effect.result,
);
// Only a clean exit is parsed. Failed invocations print help or error text that
// must not be read as model slugs or as a login verdict.
const modelsOutput =
Result.isSuccess(modelsResult) &&
Option.isSome(modelsResult.success) &&
modelsResult.success.value.code === 0
? modelsResult.success.value
: undefined;
const cliModels: GrokModelsCliOutput = modelsOutput
? parseGrokModelsCliOutput(`${modelsOutput.stdout}\n${modelsOutput.stderr}`)
: { authenticated: null, models: [] };
if (!modelsOutput) {
yield* Effect.logWarning("Grok CLI model listing failed or timed out.", {
errorTag: Result.isFailure(modelsResult)
? modelsResult.failure._tag
: Option.isNone(modelsResult.success)
? "Timeout"
: `ExitCode${modelsResult.success.value.code}`,
});
}
return cliModels;
});
const decodeAvailableCommands = Schema.decodeUnknownOption(Schema.Array(Schema.Unknown));
const decodeAvailableCommand = Schema.decodeUnknownOption(
Schema.Struct({
name: Schema.String,
description: Schema.String,
input: Schema.optional(Schema.NullOr(Schema.Struct({ hint: Schema.String }))),
}),
);
export function grokSlashCommandsFromInitialize(
initialized: EffectAcpSchema.InitializeResponse,
): ReadonlyArray<ServerProviderSlashCommand> {
const commands = decodeAvailableCommands(initialized._meta?.availableCommands);
const byName = new Map<string, ServerProviderSlashCommand>([
[COMPACT_SLASH_COMMAND.name, COMPACT_SLASH_COMMAND],
]);
for (const entry of Option.getOrElse(commands, () => [])) {
const decoded = decodeAvailableCommand(entry);
if (Option.isNone(decoded)) continue;
const command = decoded.value;
const name = command.name.trim();
if (!name || name.toLowerCase() === "always-approve") continue;
// Grok advertises /context, but its ACP handler completes without emitting output.
if (name.toLowerCase() === "context") continue;
const description = command.description.trim();
const hint = command.input?.hint.trim();
byName.set(name, {
name,
...(description ? { description } : {}),
...(hint ? { input: { hint } } : {}),
});
}
return [...byName.values()];
}
/**
* This never calls `authenticate` or `session/new`, so it cannot open a browser login or
* boot the workspace's MCP servers.
*/
const discoverGrokMetadataViaAcpInitialize = Effect.fn(
"GrokProviderStatus.discoverGrokMetadataViaAcpInitialize",
)(function* (grokSettings: GrokSettings, environment: NodeJS.ProcessEnv) {
const acp = yield* makeGrokAcpRuntime({
grokSettings,
environment,
cwd: process.cwd(),
clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },
});
const initialized = yield* acp.initialize();
return {
models: buildGrokModelsFromSessionModelState(sessionModelStateFromInitialize(initialized)),
slashCommands: grokSlashCommandsFromInitialize(initialized),
};
}, Effect.scoped);
export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* (
grokSettings: GrokSettings,
environment: NodeJS.ProcessEnv = process.env,
cwd?: string,
): Effect.fn.Return<
ServerProviderDraft,
never,
ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto
> {
const checkedAt = DateTime.formatIso(yield* DateTime.now);
const fallbackModels = grokModelsFromSettings(grokSettings.customModels);
if (!grokSettings.enabled) {
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: false,
checkedAt,
models: fallbackModels,
probe: {
installed: false,
version: null,
status: "warning",
auth: { status: "unknown" },
message: "Grok is disabled in T3 Code settings.",
},
});
}
const probeEnvironment = grokProbeEnvironment(environment);
const apiKey = environment[GROK_API_KEY_ENV]?.trim();
const versionResult = yield* runGrokCliCommand(
grokSettings,
["--version"],
probeEnvironment,
).pipe(Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), Effect.result);
if (Result.isFailure(versionResult)) {
const error = versionResult.failure;
yield* Effect.logWarning("Grok CLI health check failed.", {
errorTag: error._tag,
});
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: grokSettings.enabled,
checkedAt,
models: fallbackModels,
probe: {
installed: !isCommandMissingCause(error),
version: null,
status: "error",
auth: { status: "unknown" },
message: isCommandMissingCause(error)
? "Grok CLI (`grok`) is not installed or not on PATH."
: "Failed to execute Grok CLI health check.",
},
});
}
if (Option.isNone(versionResult.success)) {
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: grokSettings.enabled,
checkedAt,
models: fallbackModels,
probe: {
installed: true,
version: null,
status: "error",
auth: { status: "unknown" },
message: "Grok CLI is installed but timed out while running `grok --version`.",
},
});
}
const versionOutput = versionResult.success.value;
const version = parseGenericCliVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`);
if (versionOutput.code !== 0) {
yield* Effect.logWarning("Grok CLI version probe exited with a non-zero status.", {
exitCode: versionOutput.code,
stdoutLength: versionOutput.stdout.length,
stderrLength: versionOutput.stderr.length,
});
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: grokSettings.enabled,
checkedAt,
models: fallbackModels,
probe: {
installed: true,
version,
status: "error",
auth: { status: "unknown" },
message: "Grok CLI is installed but failed to run.",
},
});
}
// `grok models` reports login state and model slugs without starting the agent.
let cliModels = yield* probeGrokModels(grokSettings, probeEnvironment);
// An expired access token prints the logged-out line, then the CLI refreshes it in the same
// process. Probing again after that process exited tells a refresh from a real logout.
if (cliModels.authenticated === false && !apiKey) {
const recheck = yield* probeGrokModels(grokSettings, probeEnvironment);
if (recheck.authenticated !== null) {
cliModels = recheck;
}
}
const auth: ServerProviderAuth = apiKey
? { status: "authenticated", type: "api_key", label: "xAI API key" }
: cliModels.authenticated === true
? { status: "authenticated", type: "cached_token", label: "Grok account" }
: cliModels.authenticated === false
? { status: "unauthenticated" }
: { status: "unknown" };
const skills = yield* discoverGrokSkills(grokSettings, environment, cwd).pipe(
Effect.tapError((cause) => Effect.logDebug("Grok skill discovery failed.", { cause })),
Effect.orElseSucceed(() => []),
);
const acpExit = yield* discoverGrokMetadataViaAcpInitialize(grokSettings, probeEnvironment).pipe(
Effect.timeoutOption(GROK_ACP_INITIALIZE_TIMEOUT_MS),
Effect.exit,
);
const acpMetadata = Exit.isSuccess(acpExit) ? Option.getOrUndefined(acpExit.value) : undefined;
const acpModels = acpMetadata?.models ?? [];
const acpFailed = Exit.isFailure(acpExit) || Option.isNone(acpExit.value);
if (acpFailed) {
yield* Effect.logWarning("Grok ACP initialize probe failed or timed out.", {
errorTag: Exit.isFailure(acpExit) ? causeErrorTag(acpExit.cause) : "Timeout",
});
}
const discoveredModels = acpModels.length > 0 ? acpModels : cliModels.models;
const models =
discoveredModels.length > 0
? grokModelsFromSettings(grokSettings.customModels, discoveredModels)
: fallbackModels;
// The registry replaces cached models on a ready probe, so a fallback-only list must not be ready.
const incompleteMessage = acpFailed
? "Grok CLI is installed but ACP initialize failed. Model options may be incomplete."
: discoveredModels.length === 0
? "Grok CLI did not list any models. Model options may be incomplete."
: undefined;
if (auth.status === "unauthenticated") {
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: grokSettings.enabled,
checkedAt,
models,
skills,
probe: {
installed: true,
version,
status: "error",
auth,
message: "Grok CLI is installed but not logged in. Run `grok login`.",
},
});
}
return buildServerProvider({
presentation: GROK_PRESENTATION,
enabled: grokSettings.enabled,
checkedAt,
models,
skills,
slashCommands: acpMetadata?.slashCommands ?? [COMPACT_SLASH_COMMAND],
probe: {
installed: true,
version,
status: incompleteMessage ? "warning" : "ready",
auth,
...(incompleteMessage ? { message: incompleteMessage } : {}),
},
});
});
export const enrichGrokSnapshot = (input: {
readonly snapshot: ServerProvider;
readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
readonly enableProviderUpdateChecks?: boolean;
readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
}): Effect.Effect<
void,
never,
HttpClient.HttpClient | ProviderLatestVersions.ProviderLatestVersions
> => {
const { snapshot, publishSnapshot } = input;
return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities, {
enableProviderUpdateChecks: input.enableProviderUpdateChecks,
}).pipe(
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
Effect.catchCause((cause) =>
Effect.logWarning("Grok version advisory enrichment failed", {
errorTag: causeErrorTag(cause),
}),
),
Effect.asVoid,
);
};