Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
84 changes: 3 additions & 81 deletions apps/server/src/provider/Drivers/ClaudeSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,9 @@ import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { parse as parseYamlDocument } from "yaml";

import { expandHomePath } from "../../pathExpansion.ts";

type ClaudeSkillScope = "user" | "project";

const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;

type SkillFrontmatter =
| { readonly kind: "missing" }
| { readonly kind: "malformed" }
| { readonly kind: "parsed"; readonly name?: string; readonly description?: string };

function parseSkillFrontmatter(contents: string): SkillFrontmatter {
const match = FRONTMATTER_PATTERN.exec(contents);
if (!match) {
return { kind: "missing" };
}

let parsed: unknown;
try {
parsed = parseYamlDocument(match[1] ?? "");
} catch {
return { kind: "malformed" };
}
if (typeof parsed !== "object" || parsed === null) {
return { kind: "malformed" };
}

const record = parsed as Record<string, unknown>;
const name = typeof record.name === "string" ? record.name.trim() : "";
const description = typeof record.description === "string" ? record.description.trim() : "";
return {
kind: "parsed",
...(name ? { name } : {}),
...(description ? { description } : {}),
};
}
import { discoverSkillsFromRoots } from "./ProviderSkills.ts";

/**
* Resolve the Claude config directory the CLI would use, matching the
Expand Down Expand Up @@ -95,54 +60,11 @@ export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function*
cwd?: string,
environment?: NodeJS.ProcessEnv,
): Effect.fn.Return<ReadonlyArray<ServerProviderSkill>, never, FileSystem.FileSystem | Path.Path> {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);

const roots: ReadonlyArray<{ directory: string; scope: ClaudeSkillScope }> = [
return yield* discoverSkillsFromRoots([
{ directory: path.join(configDirPath, "skills"), scope: "user" },
...(cwd ? [{ directory: path.join(cwd, ".claude", "skills"), scope: "project" as const }] : []),
];

const skillsByName = new Map<string, ServerProviderSkill>();
for (const root of roots) {
const entries = yield* fileSystem
.readDirectory(root.directory)
.pipe(Effect.orElseSucceed((): ReadonlyArray<string> => []));

for (const entry of [...entries].sort()) {
const skillPath = path.join(root.directory, entry, "SKILL.md");
const contents = yield* fileSystem
.readFileString(skillPath)
.pipe(Effect.orElseSucceed(() => undefined));
if (contents === undefined) {
continue;
}

const frontmatter = parseSkillFrontmatter(contents);
// Malformed frontmatter means the skill won't load in Claude Code
// either — skip it rather than surfacing a broken entry under its
// directory name.
if (frontmatter.kind === "malformed") {
continue;
}

const name = (frontmatter.kind === "parsed" ? frontmatter.name : undefined) ?? entry.trim();
if (!name) {
continue;
}

skillsByName.set(name, {
name,
path: skillPath,
enabled: true,
scope: root.scope,
...(frontmatter.kind === "parsed" && frontmatter.description
? { description: frontmatter.description }
: {}),
});
}
}

return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
]);
});
7 changes: 6 additions & 1 deletion apps/server/src/provider/Drivers/GrokDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { cwd } = yield* ServerConfig;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
Expand Down Expand Up @@ -113,10 +116,12 @@ 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),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
Expand Down
94 changes: 94 additions & 0 deletions apps/server/src/provider/Drivers/GrokSkills.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

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

const writeSkill = Effect.fn("writeGrokSkill")(function* (
root: string,
directoryName: string,
description: string,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const skillDirectory = path.join(root, directoryName);
yield* fs.makeDirectory(skillDirectory, { recursive: true });
yield* fs.writeFileString(
path.join(skillDirectory, "SKILL.md"),
["---", `name: ${directoryName}`, `description: ${description}`, "---"].join("\n"),
);
});

it.layer(NodeServices.layer)("discoverGrokSkills", (it) => {
it.effect("scans Grok user and project roots in override order", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-grok-skills-" });
const homeDirectory = path.join(tempDir, "home");
const cwd = path.join(tempDir, "workspace");
const roots = [
path.join(homeDirectory, ".agents", "skills"),
path.join(homeDirectory, ".grok", "skills"),
path.join(cwd, ".agents", "skills"),
path.join(cwd, ".grok", "skills"),
];

yield* Effect.forEach(roots, (root, index) => writeSkill(root, "shared", `root-${index}`), {
discard: true,
});
yield* writeSkill(roots[0]!, "agents-user", "From user agents.");
yield* writeSkill(roots[1]!, "grok-user", "From user Grok.");
yield* writeSkill(roots[2]!, "agents-project", "From project agents.");
yield* writeSkill(roots[3]!, "grok-project", "From project Grok.");

const skills = yield* discoverGrokSkills(cwd, {}, homeDirectory);

assert.deepEqual(
skills.map(({ name, scope, description }) => ({ name, scope, description })),
[
{ name: "agents-project", scope: "project", description: "From project agents." },
{ name: "agents-user", scope: "user", description: "From user agents." },
{ name: "grok-project", scope: "project", description: "From project Grok." },
{ name: "grok-user", scope: "user", description: "From user Grok." },
{ name: "shared", scope: "project", description: "root-3" },
],
);
assert.equal(
skills.find((skill) => skill.name === "shared")?.path,
path.join(roots[3]!, "shared", "SKILL.md"),
);
}),
);

it.effect("uses USERPROFILE as the user home on Windows", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-grok-windows-home-" });
const userProfile = path.join(tempDir, "user-profile");
const fallbackHome = path.join(tempDir, "server-home");
const skillsRoot = path.join(userProfile, ".agents", "skills");
yield* writeSkill(skillsRoot, "windows-user", "From USERPROFILE.");

const skills = yield* discoverGrokSkills(
undefined,
{ USERPROFILE: userProfile },
fallbackHome,
).pipe(Effect.provideService(HostProcessPlatform, "win32"));

assert.deepEqual(skills, [
{
name: "windows-user",
description: "From USERPROFILE.",
path: path.join(skillsRoot, "windows-user", "SKILL.md"),
scope: "user",
enabled: true,
},
]);
}),
);
});
44 changes: 44 additions & 0 deletions apps/server/src/provider/Drivers/GrokSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as NodeOS from "node:os";

import type { ServerProviderSkill } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

import { discoverSkillsFromRoots } from "./ProviderSkills.ts";

export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* (
cwd?: string,
environment: NodeJS.ProcessEnv = process.env,
fallbackHomeDirectory = NodeOS.homedir(),
): Effect.fn.Return<ReadonlyArray<ServerProviderSkill>, never, FileSystem.FileSystem | Path.Path> {
const path = yield* Path.Path;
const platform = yield* HostProcessPlatform;
const environmentHome =
(platform === "win32" ? environment.USERPROFILE : environment.HOME)?.trim() ?? "";
const homeDirectory =
environmentHome.length > 0
? cwd
? path.resolve(cwd, environmentHome)
: path.resolve(environmentHome)
: fallbackHomeDirectory;
const environmentGrokHome = environment.GROK_HOME?.trim() ?? "";
const grokHome =
environmentGrokHome.length > 0
? cwd
? path.resolve(cwd, environmentGrokHome)
: path.resolve(environmentGrokHome)
: path.join(homeDirectory, ".grok");

return yield* discoverSkillsFromRoots([
{ directory: path.join(homeDirectory, ".agents", "skills"), scope: "user" },
{ directory: path.join(grokHome, "skills"), scope: "user" },
...(cwd
? [
{ directory: path.join(cwd, ".agents", "skills"), scope: "project" },
{ directory: path.join(cwd, ".grok", "skills"), scope: "project" },
]
: []),
]);
});
111 changes: 111 additions & 0 deletions apps/server/src/provider/Drivers/ProviderSkills.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

import { discoverSkillsFromRoots } from "./ProviderSkills.ts";

it.layer(NodeServices.layer)("discoverSkillsFromRoots", (it) => {
it.effect("discovers skill metadata from each root", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-skills-" });
const userRoot = path.join(tempDir, "user-skills");
const projectRoot = path.join(tempDir, "project-skills");
const userSkillPath = path.join(userRoot, "review", "SKILL.md");
const projectSkillPath = path.join(projectRoot, "deploy", "SKILL.md");

yield* fs.makeDirectory(path.dirname(userSkillPath), { recursive: true });
yield* fs.writeFileString(
userSkillPath,
["---", "name: review", "description: Review the change.", "---"].join("\n"),
);
yield* fs.makeDirectory(path.dirname(projectSkillPath), { recursive: true });
yield* fs.writeFileString(
projectSkillPath,
["---", "name: deploy", "description: Deploy the app.", "---"].join("\n"),
);

const skills = yield* discoverSkillsFromRoots([
{ directory: userRoot, scope: "user" },
{ directory: projectRoot, scope: "project" },
]);

assert.deepEqual(skills, [
{
name: "deploy",
description: "Deploy the app.",
path: projectSkillPath,
scope: "project",
enabled: true,
},
{
name: "review",
description: "Review the change.",
path: userSkillPath,
scope: "user",
enabled: true,
},
]);
}),
);

it.effect("lets later roots win and skips missing roots and malformed frontmatter", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-skills-" });
const userRoot = path.join(tempDir, "user-skills");
const projectRoot = path.join(tempDir, "project-skills");
const userSkillPath = path.join(userRoot, "deploy", "SKILL.md");
const projectSkillPath = path.join(projectRoot, "deploy", "SKILL.md");
const malformedSkillPath = path.join(projectRoot, "broken", "SKILL.md");
const unterminatedSkillPath = path.join(projectRoot, "unterminated", "SKILL.md");
const nonStringFieldsSkillPath = path.join(projectRoot, "fallback-name", "SKILL.md");

yield* fs.makeDirectory(path.dirname(userSkillPath), { recursive: true });
yield* fs.writeFileString(
userSkillPath,
["---", "name: deploy", "description: User deploy.", "---"].join("\n"),
);
yield* fs.makeDirectory(path.dirname(projectSkillPath), { recursive: true });
yield* fs.writeFileString(
projectSkillPath,
["---", "name: deploy", "description: Project deploy.", "---"].join("\n"),
);
yield* fs.makeDirectory(path.dirname(malformedSkillPath), { recursive: true });
yield* fs.writeFileString(malformedSkillPath, "---\nname: [unclosed\n---\n");
yield* fs.makeDirectory(path.dirname(unterminatedSkillPath), { recursive: true });
yield* fs.writeFileString(unterminatedSkillPath, "---\nname: unfinished\n");
yield* fs.makeDirectory(path.dirname(nonStringFieldsSkillPath), { recursive: true });
yield* fs.writeFileString(
nonStringFieldsSkillPath,
["---", "name: 2024", "description:", " nested: value", "---"].join("\n"),
);

const skills = yield* discoverSkillsFromRoots([
{ directory: path.join(tempDir, "missing"), scope: "user" },
{ directory: userRoot, scope: "user" },
{ directory: projectRoot, scope: "project" },
]);

assert.deepEqual(skills, [
{
name: "deploy",
description: "Project deploy.",
path: projectSkillPath,
scope: "project",
enabled: true,
},
{
name: "fallback-name",
path: nonStringFieldsSkillPath,
scope: "project",
enabled: true,
},
]);
}),
);
});
Loading
Loading