/** * GrokSkills — skill discovery for the `$` picker via `grok inspect --json`. * * Unlike Claude Code, the Grok CLI reports its full skill catalog itself: * `grok inspect --json` returns `skills[]` with `name`, `description`, * `source.type` (`user` / `project` / `bundled` / `plugin`), `source.path` * (the absolute `SKILL.md` path), and `userInvocable`. Asking the CLI beats * scanning the filesystem because the catalog honors Grok's own skill config * (ignore lists, disabled skills) and includes plugin skills, which live * three levels deep under `~/.grok/installed-plugins/` where a flat scan * cannot see them. This mirrors how the Codex app-server reports skills over * `skills/list`. Discovery is best-effort: an older CLI without `inspect`, * a timeout, or malformed output yields an empty list, never a degraded * provider snapshot. * * @module provider/Drivers/GrokSkills */ import type { GrokSettings, ServerProviderSkill } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Result from "effect/Result"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { spawnAndCollect } from "../providerSnapshot.ts"; const GROK_SKILLS_PROBE_TIMEOUT_MS = 4_000; /** * Map `grok inspect --json` output onto provider skills. Entries without a * name or a filesystem path are skipped; `userInvocable: false` skills are * kept but disabled so pickers that filter on `enabled` hide them. */ export function parseGrokInspectSkills(stdout: string): ReadonlyArray { let parsed: unknown; try { parsed = JSON.parse(stdout); } catch { return []; } if (typeof parsed !== "object" || parsed === null) { return []; } const entries = (parsed as Record).skills; if (!Array.isArray(entries)) { return []; } const skillsByName = new Map(); for (const entry of entries) { if (typeof entry !== "object" || entry === null) { continue; } const record = entry as Record; const name = typeof record.name === "string" ? record.name.trim() : ""; const source = typeof record.source === "object" && record.source !== null ? (record.source as Record) : undefined; const path = typeof source?.path === "string" ? source.path.trim() : ""; if (!name || !path) { continue; } const scope = typeof source?.type === "string" ? source.type.trim() : ""; const description = typeof record.description === "string" ? record.description.trim() : ""; skillsByName.set(name, { name, path, enabled: record.userInvocable !== false, ...(scope ? { scope } : {}), ...(description ? { description } : {}), }); } return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name)); } /** * Run `grok inspect --json` and map the reported catalog onto provider * skills. Never fails: any spawn error, non-zero exit, or timeout resolves * to an empty list. */ export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* ( grokSettings: Pick, environment: NodeJS.ProcessEnv = process.env, cwd?: string, ): Effect.fn.Return< ReadonlyArray, never, ChildProcessSpawner.ChildProcessSpawner > { const command = grokSettings.binaryPath || "grok"; const inspectResult = yield* Effect.gen(function* () { const spawnCommand = yield* resolveSpawnCommand(command, ["inspect", "--json"], { env: environment, }); return yield* spawnAndCollect( command, ChildProcess.make(spawnCommand.command, spawnCommand.args, { ...(cwd ? { cwd } : {}), env: environment, shell: spawnCommand.shell, }), ); }).pipe(Effect.timeoutOption(GROK_SKILLS_PROBE_TIMEOUT_MS), Effect.result); if (Result.isFailure(inspectResult) || Option.isNone(inspectResult.success)) { yield* Effect.logDebug("Grok skill discovery failed; continuing without skills."); return []; } const output = inspectResult.success.value; if (output.code !== 0) { yield* Effect.logDebug("Grok skill discovery exited non-zero; continuing without skills.", { exitCode: output.code, }); return []; } return parseGrokInspectSkills(output.stdout); });