Skip to content
Draft
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
14 changes: 13 additions & 1 deletion apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ import {
} from "../ProviderDriver.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import { withInstanceIdentity } from "./instanceIdentity.ts";
import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts";
import {
discoverAntigravitySkills,
listAntigravitySkillCatalogRoots,
resolveAntigravityUserHome,
} from "./AntigravitySkills.ts";

const DRIVER = ProviderDriverKind.make("antigravity");
const decodeSettings = Schema.decodeSync(AntigravitySettings);
Expand Down Expand Up @@ -471,6 +475,14 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
}),
),
),
skillCatalogRoots: (cwd) =>
Effect.succeed(
listAntigravitySkillCatalogRoots({
path,
cwd,
userHome,
}),
),
adapter,
textGeneration,
auth: authFlow.controller,
Expand Down
18 changes: 18 additions & 0 deletions apps/server/src/provider/Drivers/AntigravitySkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ export function antigravityUserSkillDirectories(
];
}

export function listAntigravitySkillCatalogRoots(input: {
readonly path: Path.Path;
readonly cwd: string;
readonly userHome: string;
}): ReadonlyArray<string> {
const [configSkills, cliSkills] = antigravityUserSkillDirectories(
input.path,
input.path.join(input.userHome, ".gemini"),
);
return [
configSkills,
input.path.resolve(input.cwd, ".gemini", "skills"),
cliSkills,
input.path.resolve(input.cwd, ".agents", "skills"),
input.path.resolve(input.cwd, ".agent", "skills"),
];
}

const MAX_SKILL_BYTES = 1_000_000;
const MAX_SCAN_BYTES = 8_000_000;
const MAX_SCAN_ENTRIES = 10_000;
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
import { discoverClaudeSkills } from "./ClaudeSkills.ts";
import { discoverClaudeSkills, listClaudeSkillCatalogRoots } from "./ClaudeSkills.ts";
const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);

const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
Expand Down Expand Up @@ -262,6 +262,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
enabled,
snapshot,
snapshotForCwd,
skillCatalogRoots: (workspaceCwd) =>
listClaudeSkillCatalogRoots(effectiveConfig, workspaceCwd, processEnv).pipe(
Effect.provideService(Path.Path, path),
),
adapter,
textGeneration,
} satisfies ProviderInstance;
Expand Down
20 changes: 19 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeSkills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

import { discoverClaudeSkills, skillOverrideSettingsPaths } from "./ClaudeSkills.ts";
import {
discoverClaudeSkills,
listClaudeSkillCatalogRoots,
skillOverrideSettingsPaths,
} from "./ClaudeSkills.ts";

const writeSkill = Effect.fn(function* (
skillsDir: string,
Expand Down Expand Up @@ -221,6 +225,20 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
}),
);

it.effect("lists the user and project skill directories Claude actually loads", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skill-roots-" });
const configDir = path.join(tempDir, "claude-home");
const workspace = path.join(tempDir, "workspace");
assert.deepEqual(yield* listClaudeSkillCatalogRoots({ homePath: configDir }, workspace), [
path.join(configDir, "skills"),
path.join(workspace, ".claude", "skills"),
]);
}),
);

it.effect("resolves a relative CLAUDE_CONFIG_DIR against the workspace cwd", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,21 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct
return path.join(NodeOS.homedir(), ".claude");
});

/**
* User and project skill directories Claude Code actually loads. Fingerprinted
* by the registry so a new folder under either root refreshes the `$` picker
* without a full provider probe.
*/
export const listClaudeSkillCatalogRoots = Effect.fn("listClaudeSkillCatalogRoots")(function* (
config: Pick<ClaudeSettings, "homePath">,
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): Effect.fn.Return<ReadonlyArray<string>, never, Path.Path> {
const path = yield* Path.Path;
const configDirPath = yield* resolveClaudeConfigDirPath(config, environment, cwd);
return [path.join(configDirPath, "skills"), path.join(cwd, ".claude", "skills")];
});

/**
* Enumerate Claude Code skills from the user config dir and the workspace
* `.claude/skills`. Discovery is best-effort: unreadable roots and malformed
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
} from "../providerUpdateSettings.ts";
import {
codexContinuationIdentity,
listCodexSkillCatalogRoots,
materializeCodexShadowHome,
resolveCodexHomeLayout,
} from "./CodexHomeLayout.ts";
Expand Down Expand Up @@ -346,6 +347,10 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
enabled,
snapshot,
snapshotForCwd,
skillCatalogRoots: (cwd) =>
Effect.succeed(
listCodexSkillCatalogRoots(pathService, homeLayout.sharedHomePath, cwd, processEnv),
),
consumeResetCredit,
adapter,
textGeneration,
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/provider/Drivers/CodexHomeLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);
const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]);
const REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = new Set(["mcp-oauth-locks"]);

export function listCodexSkillCatalogRoots(
path: Path.Path,
sharedHomePath: string,
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): ReadonlyArray<string> {
const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir();
return [
path.join(sharedHomePath, "skills"),
path.join(sharedHomePath, "plugins"),
path.join(userHome, ".agents", "skills"),
path.join(cwd, ".codex", "skills"),
path.join(cwd, ".agents", "skills"),
];
}

function resolveHomePath(path: Path.Path, value: string | undefined): string {
const expanded =
value && value.trim().length > 0
Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ import {
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { discoverCursorSkills, probeCursorSkills } from "./CursorSkills.ts";
import {
discoverCursorSkills,
listCursorSkillCatalogRoots,
probeCursorSkills,
} from "./CursorSkills.ts";
const decodeCursorSettings = Schema.decodeSync(CursorSettings);

const DRIVER_KIND = ProviderDriverKind.make("cursor");
Expand Down Expand Up @@ -234,6 +238,8 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
),
Effect.flatMap((skills) => snapshotForCwd(cwd, skills)),
),
skillCatalogRoots: (cwd) =>
Effect.succeed(listCursorSkillCatalogRoots(path, cwd, processEnv)),
adapter,
textGeneration,
} satisfies ProviderInstance;
Expand Down
28 changes: 22 additions & 6 deletions apps/server/src/provider/Drivers/CursorSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,19 +216,35 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (
return skills;
});

const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* (
cwd?: string,
environment: NodeJS.ProcessEnv = process.env,
) {
const path = yield* Path.Path;
function cursorSkillRootSpecs(
path: Path.Path,
cwd: string | undefined,
environment: NodeJS.ProcessEnv,
): ReadonlyArray<{ readonly directory: string; readonly scope: "user" | "project" }> {
const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir();
const rootsBelow = (base: string, scope: "user" | "project") => [
{ directory: path.join(base, ".cursor", "skills"), scope },
{ directory: path.join(base, ".agents", "skills"), scope },
{ directory: path.join(base, ".codex", "skills"), scope },
{ directory: path.join(base, ".claude", "skills"), scope },
];
const roots = [...(cwd ? rootsBelow(cwd, "project") : []), ...rootsBelow(userHome, "user")];
return [...(cwd ? rootsBelow(cwd, "project") : []), ...rootsBelow(userHome, "user")];
}

export function listCursorSkillCatalogRoots(
path: Path.Path,
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): ReadonlyArray<string> {
return cursorSkillRootSpecs(path, cwd, environment).map((root) => root.directory);
}

const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* (
cwd?: string,
environment: NodeJS.ProcessEnv = process.env,
) {
const path = yield* Path.Path;
const roots = cursorSkillRootSpecs(path, cwd, environment);

const skillsByName = new Map<string, ServerProviderSkill>();
const budget: CursorSkillScanBudget = {
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/provider/Drivers/GrokDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from "../ProviderDriver.ts";
import { withInstanceIdentity } from "./instanceIdentity.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import { discoverGrokSkills } from "./GrokSkills.ts";
import { discoverGrokSkills, listGrokSkillCatalogRoots } from "./GrokSkills.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
Expand Down Expand Up @@ -164,6 +164,8 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
enabled,
snapshot,
snapshotForCwd,
skillCatalogRoots: (workspaceCwd) =>
Effect.succeed(listGrokSkillCatalogRoots(path, workspaceCwd, processEnv)),
adapter,
textGeneration,
} satisfies ProviderInstance;
Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/provider/Drivers/GrokSkills.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Path from "effect/Path";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import { ChildProcessSpawner } from "effect/unstable/process";

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

const inspectPayload = (skills: ReadonlyArray<unknown>) => JSON.stringify({ skills });

Expand All @@ -28,6 +30,26 @@ const makeInspectSpawner = (stdout: string, exitCode = 0, spawnCwds?: Array<stri
);
});

describe("listGrokSkillCatalogRoots", () => {
it.effect("includes grok home, plugins, shared agents, and workspace roots", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
expect(
listGrokSkillCatalogRoots(path, "/workspaces/demo", {
GROK_HOME: "/opt/grok-data",
HOME: "/home/dev",
}),
).toEqual([
path.join("/opt/grok-data", "skills"),
path.join("/opt/grok-data", "installed-plugins"),
path.join("/home/dev", ".agents", "skills"),
path.join("/workspaces/demo", ".grok", "skills"),
path.join("/workspaces/demo", ".agents", "skills"),
]);
}).pipe(Effect.provide(NodeServices.layer)),
);
});

describe("discoverGrokSkills", () => {
it.effect("maps inspect entries onto provider skills, sorted by name", () =>
Effect.gen(function* () {
Expand Down
24 changes: 24 additions & 0 deletions apps/server/src/provider/Drivers/GrokSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
*
* @module provider/Drivers/GrokSkills
*/
import * as NodeOS from "node:os";

import type { GrokSettings, ServerProviderSkill } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import type * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { ChildProcess } from "effect/unstable/process";
import { resolveSpawnCommand } from "@t3tools/shared/shell";
Expand All @@ -26,6 +29,27 @@ import { spawnAndCollect } from "../providerSnapshot.ts";

const GROK_SKILLS_PROBE_TIMEOUT_MS = 4_000;

/**
* Directories `grok inspect` can surface skills from. Plugin skills live
* under `installed-plugins`, which a flat `skills/` scan would miss, so that
* tree is fingerprinted too.
*/
export function listGrokSkillCatalogRoots(
path: Path.Path,
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): ReadonlyArray<string> {
const grokHome = environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok");
const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir();
return [
path.join(grokHome, "skills"),
path.join(grokHome, "installed-plugins"),
path.join(userHome, ".agents", "skills"),
path.join(cwd, ".grok", "skills"),
path.join(cwd, ".agents", "skills"),
];
}

class GrokSkillsProbeError extends Schema.TaggedError<GrokSkillsProbeError>()(
"GrokSkillsProbeError",
{
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*
* @module provider/Drivers/OpenCodeDriver
*/
import * as NodeOS from "node:os";

import { OpenCodeSettings, ProviderDriverKind } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -61,6 +63,22 @@ const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);

const DRIVER_KIND = ProviderDriverKind.make("opencode");

export function listOpenCodeSkillCatalogRoots(
path: Path.Path,
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): ReadonlyArray<string> {
const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir();
const configHome = environment.XDG_CONFIG_HOME?.trim() || path.join(userHome, ".config");
return [
path.join(cwd, ".opencode", "skills"),
path.join(cwd, ".agents", "skills"),
path.join(configHome, "opencode", "skills"),
path.join(userHome, ".opencode", "skills"),
path.join(userHome, ".agents", "skills"),
];
}

function isOpenCodeNativeCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
Expand Down Expand Up @@ -287,6 +305,8 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
}),
),
),
skillCatalogRoots: (cwd) =>
Effect.succeed(listOpenCodeSkillCatalogRoots(pathService, cwd, processEnv)),
adapter,
textGeneration,
} satisfies ProviderInstance;
Expand Down
Loading
Loading