Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/server/src/provider/Drivers/GrokDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const { cwd } = yield* ServerConfig;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
Expand All @@ -113,7 +114,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
});
const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv).pipe(
const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Expand Down
182 changes: 182 additions & 0 deletions apps/server/src/provider/Drivers/GrokSkills.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

import { discoverGrokSkills, parseGrokInspectSkills } from "./GrokSkills.ts";

const inspectReport = JSON.stringify({
grokVersion: "1.0.4",
skills: [
{
name: "review-pr",
description: "Review a pull request.",
source: { type: "project", path: "/repo/.grok/skills/review-pr/SKILL.md" },
userInvocable: true,
},
{
name: "commit",
description: "Create conventional commits.",
source: { type: "user", path: "/home/user/.grok/skills/commit/SKILL.md" },
userInvocable: true,
},
{
name: "canvas",
source: { type: "bundled", path: "/home/user/.grok/bundled/skills/canvas/SKILL.md" },
userInvocable: false,
},
],
});

describe("parseGrokInspectSkills", () => {
it("maps inspect skill entries and sorts them by name", () => {
const skills = parseGrokInspectSkills(inspectReport);
expect(skills).toEqual([
{
name: "canvas",
path: "/home/user/.grok/bundled/skills/canvas/SKILL.md",
enabled: true,
scope: "bundled",
},
{
name: "commit",
path: "/home/user/.grok/skills/commit/SKILL.md",
enabled: true,
scope: "user",
description: "Create conventional commits.",
},
{
name: "review-pr",
path: "/repo/.grok/skills/review-pr/SKILL.md",
enabled: true,
scope: "project",
description: "Review a pull request.",
},
]);
});

it("marks skills reported as disabled", () => {
const skills = parseGrokInspectSkills(
JSON.stringify({
skills: [
{
name: "wip-skill",
disabled: true,
source: { type: "user", path: "/home/user/.grok/skills/wip-skill/SKILL.md" },
},
],
}),
);
expect(skills).toEqual([
{
name: "wip-skill",
path: "/home/user/.grok/skills/wip-skill/SKILL.md",
enabled: false,
scope: "user",
},
]);
});

it("skips entries without a name or SKILL.md path", () => {
const skills = parseGrokInspectSkills(
JSON.stringify({
skills: [
{ name: " ", source: { type: "user", path: "/skills/a/SKILL.md" } },
{ name: "no-source" },
{ name: "no-path", source: { type: "user" } },
"not-an-object",
{ name: "kept", source: { type: "user", path: "/skills/kept/SKILL.md" } },
],
}),
);
expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
});

it("returns an empty list for malformed or unexpected payloads", () => {
expect(parseGrokInspectSkills("not json")).toEqual([]);
expect(parseGrokInspectSkills("null")).toEqual([]);
expect(parseGrokInspectSkills(JSON.stringify({ skills: "nope" }))).toEqual([]);
expect(parseGrokInspectSkills(JSON.stringify({ grokVersion: "1.0.4" }))).toEqual([]);
});
});

it.layer(NodeServices.layer)("discoverGrokSkills", (it) => {
const writeFakeGrok = (script: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-grok-skills-" });
const grokPath = path.join(dir, "grok");
yield* fs.writeFileString(grokPath, script);
yield* fs.chmod(grokPath, 0o755);
return grokPath;
});

it.effect("returns the skills reported by `grok inspect --json`", () =>
Effect.scoped(
Effect.gen(function* () {
const grokPath = yield* writeFakeGrok(
[
"#!/bin/sh",
'if [ "$1" = "inspect" ] && [ "$2" = "--json" ]; then',
` cat <<'REPORT'`,
inspectReport,
"REPORT",
" exit 0",
"fi",
"exit 1",
"",
].join("\n"),
);

const skills = yield* discoverGrokSkills({ binaryPath: grokPath }, undefined);
expect(skills.map((skill) => skill.name)).toEqual(["canvas", "commit", "review-pr"]);
}),
),
);

it.effect("runs the probe in the provided workspace directory", () =>
Effect.scoped(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const workspace = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-grok-workspace-" });
const grokPath = yield* writeFakeGrok(
[
"#!/bin/sh",
`printf '{"skills":[{"name":"where","source":{"type":"local","path":"'%s'/SKILL.md"}}]}' "$(pwd)"`,
"",
].join("\n"),
);

const skills = yield* discoverGrokSkills({ binaryPath: grokPath }, workspace);
expect(skills).toHaveLength(1);
// Compare against the resolved path: on macOS the temp dir is a
// symlink (/var -> /private/var), and `pwd` reports the real one.
const realWorkspace = yield* fs.realPath(workspace);
expect(skills[0]?.path).toBe(path.join(realWorkspace, "SKILL.md"));
}),
),
);

it.effect("returns an empty list when the probe exits non-zero", () =>
Effect.scoped(
Effect.gen(function* () {
const grokPath = yield* writeFakeGrok(["#!/bin/sh", "exit 3", ""].join("\n"));
const skills = yield* discoverGrokSkills({ binaryPath: grokPath }, undefined);
expect(skills).toEqual([]);
}),
),
);

it.effect("returns an empty list when the binary is missing", () =>
Effect.gen(function* () {
const skills = yield* discoverGrokSkills(
{ binaryPath: "/definitely/not/installed/grok-binary" },
undefined,
);
expect(skills).toEqual([]);
}),
);
});
109 changes: 109 additions & 0 deletions apps/server/src/provider/Drivers/GrokSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* GrokSkills — skill discovery for the `$` picker via `grok inspect --json`.
*
* Grok's native discovery spans local, repo, and user `.grok` and `.agents`
* roots, Claude and Cursor compatibility directories, bundled and plugin
* skills, and `[skills]` config overrides, deduplicating by name across all
* of them. Rather than replicate those semantics on the filesystem, discovery
* asks the CLI for its own post-dedup inventory, the same way the Codex
* app-server reports its skills. Discovery is best-effort: a missing binary,
* timeout, or malformed report 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 { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { resolveSpawnCommand } from "@t3tools/shared/shell";

import { spawnAndCollect } from "../providerSnapshot.ts";

const GROK_SKILLS_PROBE_TIMEOUT_MS = 5_000;

/**
* Parse the `skills` section of a `grok inspect --json` report. Entries
* missing a name or a `SKILL.md` path are skipped; anything else about the
* payload being unexpected yields an empty list.
*/
export function parseGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSkill> {
let parsed: unknown;
try {
parsed = JSON.parse(stdout);
} catch {
return [];
}
if (typeof parsed !== "object" || parsed === null) {
return [];
}

const skills = (parsed as Record<string, unknown>).skills;
if (!globalThis.Array.isArray(skills)) {
return [];
}

const parsedSkills: ServerProviderSkill[] = [];
for (const entry of skills) {
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() : "";
parsedSkills.push({
name,
path,
enabled: record.disabled !== true,
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
...(scope ? { scope } : {}),
...(description ? { description } : {}),
});
Comment thread
cursor[bot] marked this conversation as resolved.
}

return parsedSkills.sort((left, right) => left.name.localeCompare(right.name));
}

/**
* Run `grok inspect --json` in the workspace so local and repo-scope skills
* resolve the same way a live Grok session would, and return the reported
* skill inventory.
*/
export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* (
grokSettings: Pick<GrokSettings, "binaryPath">,
cwd: string | undefined,
environment: NodeJS.ProcessEnv = process.env,
): Effect.fn.Return<
ReadonlyArray<ServerProviderSkill>,
never,
ChildProcessSpawner.ChildProcessSpawner
> {
const command = grokSettings.binaryPath || "grok";
return yield* Effect.gen(function* () {
const spawnCommand = yield* resolveSpawnCommand(command, ["inspect", "--json"], {
env: environment,
});
const result = yield* spawnAndCollect(
command,
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
env: environment,
shell: spawnCommand.shell,
...(cwd ? { cwd } : {}),
}),
);
return result.code === 0 ? parseGrokInspectSkills(result.stdout) : [];
}).pipe(
Effect.timeoutOption(GROK_SKILLS_PROBE_TIMEOUT_MS),
Effect.map(Option.getOrElse((): ReadonlyArray<ServerProviderSkill> => [])),
Effect.orElseSucceed((): ReadonlyArray<ServerProviderSkill> => []),
);
});
42 changes: 42 additions & 0 deletions apps/server/src/provider/Layers/GrokProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,46 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => {
expect(snapshot.message).toContain("ACP startup failed");
}),
);

it.effect("surfaces skills from `grok inspect --json` even when ACP startup fails", () =>
Effect.gen(function* () {
const snapshot = yield* Effect.scoped(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-grok-skills-" });
const grokPath = path.join(dir, "grok");
yield* fs.writeFileString(
grokPath,
[
"#!/bin/sh",
'if [ "$1" = "--version" ]; then printf "grok-cli 0.0.99\\n"; exit 0; fi',
'if [ "$1" = "inspect" ] && [ "$2" = "--json" ]; then',
` printf '%s' '{"skills":[{"name":"commit","description":"Commit helper.","source":{"type":"user","path":"/home/user/.grok/skills/commit/SKILL.md"},"userInvocable":true}]}'`,
" exit 0",
"fi",
"exit 1",
"",
].join("\n"),
);
yield* fs.chmod(grokPath, 0o755);

return yield* checkGrokProviderStatus(
decodeGrokSettings({ enabled: true, binaryPath: grokPath }),
);
}),
);

expect(snapshot.installed).toBe(true);
expect(snapshot.skills).toEqual([
{
name: "commit",
path: "/home/user/.grok/skills/commit/SKILL.md",
enabled: true,
scope: "user",
description: "Commit helper.",
},
]);
}),
);
});
Loading
Loading