Open History to see the last commit
1import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe";
2// The CLI catalog honors ignored/disabled skills and nested plugin skills that flat scans miss.
3import type { ServerProviderSkill } from "@t3tools/contracts";
4import type { GrokSettings } from "../settings.ts";
5import * as Effect from "effect/Effect";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as ChildProcess from "effect/process/ChildProcess";
9import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";
10import { resolveSpawnCommand } from "@t3tools/shared/shell";
11
12import { spawnAndCollect } from "@t3tools/provider-core/server/snapshotProbe";
13import { grokProbeEnvironment } from "./probeEnvironment.ts";
14
15const GROK_SKILLS_PROBE_TIMEOUT_MS = 4_000;
16// The composer's chip rule, anchored to the message start, so `$25` never names a skill.
17const LEADING_SKILL_MENTION = new RegExp(`^\\s*${SKILL_MENTION_PATTERN.source}`, "u");
18
19/** A message opening with a mention, such as `deploy` and ` ship it` for `$deploy ship it`. */
20export function leadingGrokSkillMention(
21 text: string,
22): { readonly name: string; readonly rest: string } | undefined {
23 const match = LEADING_SKILL_MENTION.exec(text);
24 if (match?.[2] === undefined) return undefined;
25 return { name: match[2], rest: text.slice(match[0].length) };
26}
27
28class GrokSkillsProbeError extends Schema.TaggedError<GrokSkillsProbeError>()(
29 "GrokSkillsProbeError",
30 {
31 stage: Schema.Literals(["spawn", "timeout", "exit", "decode"]),
32 cwd: Schema.optional(Schema.String),
33 exitCode: Schema.optional(Schema.Number),
34 cause: Schema.optional(Schema.Defect()),
35 },
36) {
37 override get message(): string {
38 const location = this.cwd === undefined ? "" : ` for '${this.cwd}'`;
39 const exitCode = this.exitCode === undefined ? "" : ` with exit code ${this.exitCode}`;
40 return `\`grok inspect --json\` failed during ${this.stage}${location}${exitCode}.`;
41 }
42}
43
44function decodeGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSkill> | undefined {
45 let parsed: unknown;
46 try {
47 parsed = JSON.parse(stdout);
48 } catch {
49 return undefined;
50 }
51 if (typeof parsed !== "object" || parsed === null) {
52 return undefined;
53 }
54 const entries = (parsed as Record<string, unknown>).skills;
55 if (!Array.isArray(entries)) {
56 return undefined;
57 }
58
59 const skillsByName = new Map<string, ServerProviderSkill>();
60 for (const entry of entries) {
61 if (typeof entry !== "object" || entry === null) {
62 continue;
63 }
64 const record = entry as Record<string, unknown>;
65 const name = typeof record.name === "string" ? record.name.trim() : "";
66 const source =
67 typeof record.source === "object" && record.source !== null
68 ? (record.source as Record<string, unknown>)
69 : undefined;
70 const path = typeof source?.path === "string" ? source.path.trim() : "";
71 if (!name || !path) {
72 continue;
73 }
74 const scope = typeof source?.type === "string" ? source.type.trim() : "";
75 const description = typeof record.description === "string" ? record.description.trim() : "";
76 skillsByName.set(name, {
77 name,
78 path,
79 enabled: record.userInvocable !== false,
80 ...(scope ? { scope } : {}),
81 ...(description ? { description } : {}),
82 });
83 }
84
85 return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
86}
87
88export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* (
89 grokSettings: Pick<GrokSettings, "binaryPath">,
90 environment: NodeJS.ProcessEnv = process.env,
91 cwd?: string,
92) {
93 const command = grokSettings.binaryPath || "grok";
94 const probeEnvironment = grokProbeEnvironment(environment);
95 const inspectResult = yield* Effect.gen(function* () {
96 const spawnCommand = yield* resolveSpawnCommand(command, ["inspect", "--json"], {
97 env: probeEnvironment,
98 });
99 return yield* spawnAndCollect(
100 command,
101 ChildProcess.make(spawnCommand.command, spawnCommand.args, {
102 ...(cwd ? { cwd } : {}),
103 env: probeEnvironment,
104 shell: spawnCommand.shell,
105 }),
106 );
107 }).pipe(
108 Effect.mapError(
109 (cause) =>
110 new GrokSkillsProbeError({
111 stage: "spawn",
112 ...(cwd ? { cwd } : {}),
113 cause,
114 }),
115 ),
116 Effect.timeoutOption(
117 resolveProviderProbeTimeoutMs("grok", GROK_SKILLS_PROBE_TIMEOUT_MS, environment),
118 ),
119 );
120
121 if (Option.isNone(inspectResult)) {
122 return yield* new GrokSkillsProbeError({
123 stage: "timeout",
124 ...(cwd ? { cwd } : {}),
125 });
126 }
127 const output = inspectResult.value;
128 if (output.code !== 0) {
129 return yield* new GrokSkillsProbeError({
130 stage: "exit",
131 ...(cwd ? { cwd } : {}),
132 exitCode: output.code,
133 });
134 }
135 const skills = decodeGrokInspectSkills(output.stdout);
136 if (!skills) {
137 return yield* new GrokSkillsProbeError({
138 stage: "decode",
139 ...(cwd ? { cwd } : {}),
140 });
141 }
142 return skills;
143});