Files
fusion/plugins/fusion-plugin-grok-runtime/src/process-manager.ts
gsxdsm 2580524421 FN-7712: fix Grok CLI model list parsing for real grok models output
Fixes the Grok CLI model picker showing raw prompt/preamble text instead of real model names by rewriting parseModelLines to match the actual verified `grok models` output shape.

- Rewrote parseModelLines in process-manager.ts to strip the login/"Default model:"/"Available models:" preamble
- Strip `*`/`-` bullet markers and the `(default)` annotation from each model line
- Preserve existing legacy `id - Label`, columnar, and JSON parsing paths
- Added regression tests covering the real grok models output shape
- Added changeset (patch) documenting the fix

Files changed:
 .changeset/fn-7712-grok-model-parse.md             |  7 ++++
 .../src/__tests__/process-manager.test.ts          | 39 ++++++++++++++++++++++
 .../src/process-manager.ts                         | 34 ++++++++++++-------
 3 files changed, 68 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-7712

Fusion-Task-Lineage: 93e34513-07b9-41b3-8b8b-ecdb763b4208

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-09 19:58:03 -07:00

97 lines
3.8 KiB
TypeScript

import { runGrokCommand } from "./cli-spawn.js";
/*
FNXC:GrokCli 2026-07-08-19:30:
FN-7712: the `grok models` output shape is now VERIFIED from a real capture
(attachment 1871.png in FN-7712): the CLI prints a login/session preamble
("You are logged in with grok..."), a "Default model: <id>" line, an
"Available models:" header, then a bulleted list — `* <id> (default)` for
the active model and `- <id>` for the rest. We drop the preamble/header
lines (case-insensitively, tolerating a trailing colon and leading/trailing
whitespace), strip a leading `* `/`- ` bullet marker from each remaining
candidate line, then strip a trailing ` (default)`/`(default)` annotation
before id extraction so the picker only ever sees bare model ids. Legacy
extraction (leading token before a ` - ` label separator, else before the
first run of 2+ spaces for columnar/pricing layouts, else the whole trimmed
token) is preserved unchanged so older `id - Label (pricing)` and columnar
fixtures still parse. Defensive JSON-tolerant parsing is attempted first
(mirroring Cursor's process-manager.ts), even though the real CLI is not
known to emit JSON.
*/
function parseModelLines(raw: string): string[] {
const ids = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => !/^available models:?$/i.test(line))
.filter((line) => !/^you are logged in\b/i.test(line))
.filter((line) => !/^default model:?/i.test(line))
.filter((line) => !/^models?:?$/i.test(line))
.filter((line) => !/^no models? available/i.test(line))
.filter((line) => !/^tip:/i.test(line))
.filter((line) => !/^usage/i.test(line))
.map((line) => line.replace(/^[*-]\s+/, ""))
.map((line) => line.replace(/\s*\(default\)\s*$/i, ""))
.map((line) => line.trim())
.map((line) => {
const dashIndex = line.indexOf(" - ");
if (dashIndex !== -1) return line.slice(0, dashIndex).trim();
const columnMatch = line.match(/\s{2,}/);
if (columnMatch && typeof columnMatch.index === "number") {
return line.slice(0, columnMatch.index).trim();
}
return line;
})
.filter(Boolean);
return Array.from(new Set(ids));
}
export interface GrokModelDiscoveryResult {
models: string[];
source: string;
fallbackUsed: boolean;
reason?: string;
}
export async function discoverGrokModels(binary: string, timeoutMs = 5000): Promise<GrokModelDiscoveryResult> {
const res = await runGrokCommand(binary, ["models"], timeoutMs);
if (res.code !== 0) {
return { models: [], source: "none", fallbackUsed: true, reason: "model discovery command unavailable" };
}
const output = (res.stdout || "").trim();
if (!output) {
return { models: [], source: "none", fallbackUsed: true, reason: "model discovery command returned no output" };
}
if (/^no models? available/i.test(output)) {
return { models: [], source: "models-text", fallbackUsed: false, reason: "no models available for this account" };
}
// Defensive fallback: tolerate output that happens to be JSON, even though
// the real CLI is not known to support a --json flag today.
try {
const parsed = JSON.parse(output);
if (Array.isArray(parsed)) {
const ids: string[] = [];
for (const entry of parsed) {
const id = typeof entry === "string" ? entry : typeof entry?.id === "string" ? entry.id : undefined;
if (id) ids.push(id);
}
if (ids.length > 0) {
return { models: Array.from(new Set(ids)), source: "models-json", fallbackUsed: false };
}
}
} catch {
// output is not JSON; fall through to line-based parsing
}
const ids = parseModelLines(output);
if (ids.length > 0) {
return { models: ids, source: "models-text", fallbackUsed: false };
}
return { models: [], source: "none", fallbackUsed: true, reason: "model discovery command unavailable" };
}