151 lines · 5.2 KB · TypeScript
RawDownload
/** * 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`. Probe failures stay typed so workspace snapshots do not * cache an empty catalog; machine-level discovery recovers them to an empty * list without degrading the provider. * * @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 Schema from "effect/Schema";import { ChildProcess } from "effect/unstable/process";import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { spawnAndCollect } from "../providerSnapshot.ts"; const GROK_SKILLS_PROBE_TIMEOUT_MS = 4_000; class GrokSkillsProbeError extends Schema.TaggedError<GrokSkillsProbeError>()(  "GrokSkillsProbeError",  {    stage: Schema.Literals(["spawn", "timeout", "exit", "decode"]),    cwd: Schema.optional(Schema.String),    exitCode: Schema.optional(Schema.Number),    cause: Schema.optional(Schema.Defect()),  },) {  override get message(): string {    const location = this.cwd === undefined ? "" : ` for '${this.cwd}'`;    const exitCode = this.exitCode === undefined ? "" : ` with exit code ${this.exitCode}`;    return `\`grok inspect --json\` failed during ${this.stage}${location}${exitCode}.`;  }} /** * 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. */function decodeGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSkill> | undefined {  let parsed: unknown;  try {    parsed = JSON.parse(stdout);  } catch {    return undefined;  }  if (typeof parsed !== "object" || parsed === null) {    return undefined;  }  const entries = (parsed as Record<string, unknown>).skills;  if (!Array.isArray(entries)) {    return undefined;  }   const skillsByName = new Map<string, ServerProviderSkill>();  for (const entry of entries) {    if (typeof entry !== "object" || entry === null) {      continue;    }    const record = entry as Record<string, unknown>;    const name = typeof record.name === "string" ? record.name.trim() : "";    const source =      typeof record.source === "object" && record.source !== null        ? (record.source as Record<string, unknown>)        : 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. Callers that need best-effort discovery can recover this effect to * an empty list; workspace callers leave failures typed so they are not cached. */export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* (  grokSettings: Pick<GrokSettings, "binaryPath">,  environment: NodeJS.ProcessEnv = process.env,  cwd?: string,) {  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.mapError(      (cause) =>        new GrokSkillsProbeError({          stage: "spawn",          ...(cwd ? { cwd } : {}),          cause,        }),    ),    Effect.timeoutOption(GROK_SKILLS_PROBE_TIMEOUT_MS),  );   if (Option.isNone(inspectResult)) {    return yield* new GrokSkillsProbeError({      stage: "timeout",      ...(cwd ? { cwd } : {}),    });  }  const output = inspectResult.value;  if (output.code !== 0) {    return yield* new GrokSkillsProbeError({      stage: "exit",      ...(cwd ? { cwd } : {}),      exitCode: output.code,    });  }  const skills = decodeGrokInspectSkills(output.stdout);  if (!skills) {    return yield* new GrokSkillsProbeError({      stage: "decode",      ...(cwd ? { cwd } : {}),    });  }  return skills;});