| author | |
| committer | |
| log | 6b9a5987f686d0d904abe6f51b4be3bf873907e9 |
| tree | 4153150c8b3020902e88da66506a0f27fd96b6b1 |
| parent | fc3f78f5d917fdeee6271d2f6fd42961369c7d9f |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>6 files changed, 368 insertions(+), 2 deletions(-)
apps/server/package.json+2-1| ... | @@ -31,7 +31,8 @@ | ... | @@ -31,7 +31,8 @@ |
| 31 | "@opencode-ai/sdk": "^1.3.15", | 31 | "@opencode-ai/sdk": "^1.3.15", |
| 32 | "@pierre/diffs": "catalog:", | 32 | "@pierre/diffs": "catalog:", |
| 33 | "effect": "catalog:", | 33 | "effect": "catalog:", |
| 34 | "node-pty": "^1.1.0" | 34 | "node-pty": "^1.1.0", |
| 35 | "yaml": "catalog:" | ||
| 35 | }, | 36 | }, |
| 36 | "devDependencies": { | 37 | "devDependencies": { |
| 37 | "@effect/vitest": "catalog:", | 38 | "@effect/vitest": "catalog:", |
apps/server/src/provider/Drivers/ClaudeDriver.ts+3| ... | @@ -118,6 +118,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { | ... | @@ -118,6 +118,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 118 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => | 118 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 119 | Effect.gen(function* () { | 119 | Effect.gen(function* () { |
| 120 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 120 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 121 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 121 | const path = yield* Path.Path; | 122 | const path = yield* Path.Path; |
| 122 | const { cwd } = yield* ServerConfig; | 123 | const { cwd } = yield* ServerConfig; |
| 123 | const httpClient = yield* HttpClient.HttpClient; | 124 | const httpClient = yield* HttpClient.HttpClient; |
| ... | @@ -165,9 +166,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { | ... | @@ -165,9 +166,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 165 | effectiveConfig, | 166 | effectiveConfig, |
| 166 | () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), | 167 | () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), |
| 167 | processEnv, | 168 | processEnv, |
| 169 | cwd, | ||
| 168 | ).pipe( | 170 | ).pipe( |
| 169 | Effect.map(stampIdentity), | 171 | Effect.map(stampIdentity), |
| 170 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), | 172 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 173 | Effect.provideService(FileSystem.FileSystem, fileSystem), | ||
| 171 | Effect.provideService(Path.Path, path), | 174 | Effect.provideService(Path.Path, path), |
| 172 | ); | 175 | ); |
| 173 | 176 |
apps/server/src/provider/Drivers/ClaudeSkills.test.ts created+205| ... | @@ -0,0 +1,205 @@ | ||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 2 | import { assert, it } from "@effect/vitest"; | ||
| 3 | import * as Effect from "effect/Effect"; | ||
| 4 | import * as FileSystem from "effect/FileSystem"; | ||
| 5 | import * as Path from "effect/Path"; | ||
| 6 | |||
| 7 | import { discoverClaudeSkills } from "./ClaudeSkills.ts"; | ||
| 8 | |||
| 9 | const writeSkill = Effect.fn(function* ( | ||
| 10 | skillsDir: string, | ||
| 11 | directoryName: string, | ||
| 12 | contents: string, | ||
| 13 | ) { | ||
| 14 | const fs = yield* FileSystem.FileSystem; | ||
| 15 | const path = yield* Path.Path; | ||
| 16 | const skillDir = path.join(skillsDir, directoryName); | ||
| 17 | yield* fs.makeDirectory(skillDir, { recursive: true }); | ||
| 18 | yield* fs.writeFileString(path.join(skillDir, "SKILL.md"), contents); | ||
| 19 | }); | ||
| 20 | |||
| 21 | it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { | ||
| 22 | it.effect("discovers user and project skills with frontmatter metadata", () => | ||
| 23 | Effect.gen(function* () { | ||
| 24 | const fs = yield* FileSystem.FileSystem; | ||
| 25 | const path = yield* Path.Path; | ||
| 26 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); | ||
| 27 | const configDir = path.join(tempDir, "claude-home"); | ||
| 28 | const workspace = path.join(tempDir, "workspace"); | ||
| 29 | |||
| 30 | yield* writeSkill( | ||
| 31 | path.join(configDir, "skills"), | ||
| 32 | "codex-review", | ||
| 33 | [ | ||
| 34 | "---", | ||
| 35 | "name: codex-review", | ||
| 36 | "description: Ask Codex for a review.", | ||
| 37 | "---", | ||
| 38 | "", | ||
| 39 | "# Body", | ||
| 40 | ].join("\n"), | ||
| 41 | ); | ||
| 42 | yield* writeSkill( | ||
| 43 | path.join(workspace, ".claude", "skills"), | ||
| 44 | "deploy", | ||
| 45 | ["---", "name: deploy", "description: Deploy the app.", "---", "", "# Deploy"].join("\n"), | ||
| 46 | ); | ||
| 47 | |||
| 48 | const skills = yield* discoverClaudeSkills({ homePath: configDir }, workspace); | ||
| 49 | |||
| 50 | assert.deepEqual(skills, [ | ||
| 51 | { | ||
| 52 | name: "codex-review", | ||
| 53 | path: path.join(configDir, "skills", "codex-review", "SKILL.md"), | ||
| 54 | enabled: true, | ||
| 55 | scope: "user", | ||
| 56 | description: "Ask Codex for a review.", | ||
| 57 | }, | ||
| 58 | { | ||
| 59 | name: "deploy", | ||
| 60 | path: path.join(workspace, ".claude", "skills", "deploy", "SKILL.md"), | ||
| 61 | enabled: true, | ||
| 62 | scope: "project", | ||
| 63 | description: "Deploy the app.", | ||
| 64 | }, | ||
| 65 | ]); | ||
| 66 | }), | ||
| 67 | ); | ||
| 68 | |||
| 69 | it.effect("prefers project skills over user skills on name collisions", () => | ||
| 70 | Effect.gen(function* () { | ||
| 71 | const fs = yield* FileSystem.FileSystem; | ||
| 72 | const path = yield* Path.Path; | ||
| 73 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); | ||
| 74 | const configDir = path.join(tempDir, "claude-home"); | ||
| 75 | const workspace = path.join(tempDir, "workspace"); | ||
| 76 | |||
| 77 | yield* writeSkill( | ||
| 78 | path.join(configDir, "skills"), | ||
| 79 | "deploy", | ||
| 80 | ["---", "name: deploy", "description: User deploy.", "---"].join("\n"), | ||
| 81 | ); | ||
| 82 | yield* writeSkill( | ||
| 83 | path.join(workspace, ".claude", "skills"), | ||
| 84 | "deploy", | ||
| 85 | ["---", "name: deploy", "description: Project deploy.", "---"].join("\n"), | ||
| 86 | ); | ||
| 87 | |||
| 88 | const skills = yield* discoverClaudeSkills({ homePath: configDir }, workspace); | ||
| 89 | |||
| 90 | assert.equal(skills.length, 1); | ||
| 91 | assert.equal(skills[0]?.scope, "project"); | ||
| 92 | assert.equal(skills[0]?.description, "Project deploy."); | ||
| 93 | }), | ||
| 94 | ); | ||
| 95 | |||
| 96 | it.effect("falls back to the directory name and skips malformed frontmatter", () => | ||
| 97 | Effect.gen(function* () { | ||
| 98 | const fs = yield* FileSystem.FileSystem; | ||
| 99 | const path = yield* Path.Path; | ||
| 100 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); | ||
| 101 | const configDir = path.join(tempDir, "claude-home"); | ||
| 102 | const skillsDir = path.join(configDir, "skills"); | ||
| 103 | |||
| 104 | yield* writeSkill(skillsDir, "no-frontmatter", "# Just a heading\n"); | ||
| 105 | yield* writeSkill(skillsDir, "broken-yaml", "---\nname: [unclosed\n---\n"); | ||
| 106 | // A stray file (not a directory with SKILL.md) must be skipped. | ||
| 107 | yield* fs.makeDirectory(skillsDir, { recursive: true }); | ||
| 108 | yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill"); | ||
| 109 | |||
| 110 | const skills = yield* discoverClaudeSkills({ homePath: configDir }, undefined); | ||
| 111 | |||
| 112 | // A skill with no frontmatter falls back to its directory name; a skill | ||
| 113 | // whose frontmatter fails to parse is skipped entirely (Claude Code | ||
| 114 | // won't load it either). | ||
| 115 | assert.deepEqual( | ||
| 116 | skills.map((skill) => skill.name), | ||
| 117 | ["no-frontmatter"], | ||
| 118 | ); | ||
| 119 | assert.equal(skills[0]?.description, undefined); | ||
| 120 | }), | ||
| 121 | ); | ||
| 122 | |||
| 123 | it.effect("honors CLAUDE_CONFIG_DIR from the environment when homePath is unset", () => | ||
| 124 | Effect.gen(function* () { | ||
| 125 | const fs = yield* FileSystem.FileSystem; | ||
| 126 | const path = yield* Path.Path; | ||
| 127 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); | ||
| 128 | const environmentConfigDir = path.join(tempDir, "env-config"); | ||
| 129 | |||
| 130 | yield* writeSkill( | ||
| 131 | path.join(environmentConfigDir, "skills"), | ||
| 132 | "env-skill", | ||
| 133 | ["---", "name: env-skill", "description: From env config dir.", "---"].join("\n"), | ||
| 134 | ); | ||
| 135 | |||
| 136 | const skills = yield* discoverClaudeSkills({ homePath: "" }, undefined, { | ||
| 137 | CLAUDE_CONFIG_DIR: environmentConfigDir, | ||
| 138 | }); | ||
| 139 | |||
| 140 | assert.deepEqual( | ||
| 141 | skills.map((skill) => skill.name), | ||
| 142 | ["env-skill"], | ||
| 143 | ); | ||
| 144 | |||
| 145 | // An explicit homePath wins over the environment variable, matching | ||
| 146 | // makeClaudeEnvironment which overwrites CLAUDE_CONFIG_DIR for the CLI. | ||
| 147 | const explicitHome = path.join(tempDir, "explicit-home"); | ||
| 148 | yield* writeSkill( | ||
| 149 | path.join(explicitHome, "skills"), | ||
| 150 | "explicit-skill", | ||
| 151 | ["---", "name: explicit-skill", "---"].join("\n"), | ||
| 152 | ); | ||
| 153 | const explicitSkills = yield* discoverClaudeSkills({ homePath: explicitHome }, undefined, { | ||
| 154 | CLAUDE_CONFIG_DIR: environmentConfigDir, | ||
| 155 | }); | ||
| 156 | assert.deepEqual( | ||
| 157 | explicitSkills.map((skill) => skill.name), | ||
| 158 | ["explicit-skill"], | ||
| 159 | ); | ||
| 160 | }), | ||
| 161 | ); | ||
| 162 | |||
| 163 | it.effect("resolves a relative CLAUDE_CONFIG_DIR against the workspace cwd", () => | ||
| 164 | Effect.gen(function* () { | ||
| 165 | const fs = yield* FileSystem.FileSystem; | ||
| 166 | const path = yield* Path.Path; | ||
| 167 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); | ||
| 168 | const workspace = path.join(tempDir, "workspace"); | ||
| 169 | yield* fs.makeDirectory(workspace, { recursive: true }); | ||
| 170 | |||
| 171 | // The spawned CLI resolves a relative CLAUDE_CONFIG_DIR against its own | ||
| 172 | // cwd (the workspace), so discovery must do the same. | ||
| 173 | yield* writeSkill( | ||
| 174 | path.join(workspace, "relative-config", "skills"), | ||
| 175 | "relative-skill", | ||
| 176 | ["---", "name: relative-skill", "---"].join("\n"), | ||
| 177 | ); | ||
| 178 | |||
| 179 | const skills = yield* discoverClaudeSkills({ homePath: "" }, workspace, { | ||
| 180 | CLAUDE_CONFIG_DIR: "relative-config", | ||
| 181 | }); | ||
| 182 | |||
| 183 | assert.deepEqual( | ||
| 184 | skills.map((skill) => skill.name), | ||
| 185 | ["relative-skill"], | ||
| 186 | ); | ||
| 187 | assert.equal(skills[0]?.scope, "user"); | ||
| 188 | }), | ||
| 189 | ); | ||
| 190 | |||
| 191 | it.effect("returns an empty list when no skill roots exist", () => | ||
| 192 | Effect.gen(function* () { | ||
| 193 | const fs = yield* FileSystem.FileSystem; | ||
| 194 | const path = yield* Path.Path; | ||
| 195 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }); | ||
| 196 | |||
| 197 | const skills = yield* discoverClaudeSkills( | ||
| 198 | { homePath: path.join(tempDir, "missing-home") }, | ||
| 199 | path.join(tempDir, "missing-workspace"), | ||
| 200 | ); | ||
| 201 | |||
| 202 | assert.deepEqual(skills, []); | ||
| 203 | }), | ||
| 204 | ); | ||
| 205 | }); | ||
apps/server/src/provider/Drivers/ClaudeSkills.ts created+148| ... | @@ -0,0 +1,148 @@ | ||
| 1 | /** | ||
| 2 | * ClaudeSkills — filesystem discovery of Claude Code skills for the `$` picker. | ||
| 3 | * | ||
| 4 | * Claude Code loads skills from `<config dir>/skills` (user scope) and | ||
| 5 | * `<cwd>/.claude/skills` (project scope), one directory per skill with a | ||
| 6 | * `SKILL.md` carrying YAML frontmatter. The Agent SDK init handshake surfaces | ||
| 7 | * skills only as slash commands without their filesystem paths, so the | ||
| 8 | * provider snapshot scans the same locations directly, mirroring how the | ||
| 9 | * Codex app-server reports its skills. | ||
| 10 | * | ||
| 11 | * @module provider/Drivers/ClaudeSkills | ||
| 12 | */ | ||
| 13 | import * as NodeOS from "node:os"; | ||
| 14 | |||
| 15 | import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts"; | ||
| 16 | import * as Effect from "effect/Effect"; | ||
| 17 | import * as FileSystem from "effect/FileSystem"; | ||
| 18 | import * as Path from "effect/Path"; | ||
| 19 | import { parse as parseYamlDocument } from "yaml"; | ||
| 20 | |||
| 21 | import { expandHomePath } from "../../pathExpansion.ts"; | ||
| 22 | |||
| 23 | type ClaudeSkillScope = "user" | "project"; | ||
| 24 | |||
| 25 | const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/; | ||
| 26 | |||
| 27 | type SkillFrontmatter = | ||
| 28 | | { readonly kind: "missing" } | ||
| 29 | | { readonly kind: "malformed" } | ||
| 30 | | { readonly kind: "parsed"; readonly name?: string; readonly description?: string }; | ||
| 31 | |||
| 32 | function parseSkillFrontmatter(contents: string): SkillFrontmatter { | ||
| 33 | const match = FRONTMATTER_PATTERN.exec(contents); | ||
| 34 | if (!match) { | ||
| 35 | return { kind: "missing" }; | ||
| 36 | } | ||
| 37 | |||
| 38 | let parsed: unknown; | ||
| 39 | try { | ||
| 40 | parsed = parseYamlDocument(match[1] ?? ""); | ||
| 41 | } catch { | ||
| 42 | return { kind: "malformed" }; | ||
| 43 | } | ||
| 44 | if (typeof parsed !== "object" || parsed === null) { | ||
| 45 | return { kind: "malformed" }; | ||
| 46 | } | ||
| 47 | |||
| 48 | const record = parsed as Record<string, unknown>; | ||
| 49 | const name = typeof record.name === "string" ? record.name.trim() : ""; | ||
| 50 | const description = typeof record.description === "string" ? record.description.trim() : ""; | ||
| 51 | return { | ||
| 52 | kind: "parsed", | ||
| 53 | ...(name ? { name } : {}), | ||
| 54 | ...(description ? { description } : {}), | ||
| 55 | }; | ||
| 56 | } | ||
| 57 | |||
| 58 | /** | ||
| 59 | * Resolve the Claude config directory the CLI would use, matching the | ||
| 60 | * precedence the spawned CLI sees: the instance's `homePath` (exported as | ||
| 61 | * `CLAUDE_CONFIG_DIR` by `makeClaudeEnvironment`), then a `CLAUDE_CONFIG_DIR` | ||
| 62 | * already present in the process environment, then `~/.claude`. | ||
| 63 | */ | ||
| 64 | const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(function* ( | ||
| 65 | config: Pick<ClaudeSettings, "homePath">, | ||
| 66 | environment: NodeJS.ProcessEnv, | ||
| 67 | cwd?: string, | ||
| 68 | ): Effect.fn.Return<string, never, Path.Path> { | ||
| 69 | const path = yield* Path.Path; | ||
| 70 | const homePath = config.homePath.trim(); | ||
| 71 | if (homePath.length > 0) { | ||
| 72 | return path.resolve(expandHomePath(homePath)); | ||
| 73 | } | ||
| 74 | // No tilde expansion here: the spawned CLI receives this env var verbatim | ||
| 75 | // (env vars are never shell-expanded), so a literal `~` must stay literal | ||
| 76 | // for discovery to scan the same directory the runtime would. A relative | ||
| 77 | // value is resolved against the workspace cwd — the subprocess's own cwd — | ||
| 78 | // for the same reason. | ||
| 79 | const environmentConfigDir = environment.CLAUDE_CONFIG_DIR?.trim() ?? ""; | ||
| 80 | if (environmentConfigDir.length > 0) { | ||
| 81 | return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir); | ||
| 82 | } | ||
| 83 | return path.join(NodeOS.homedir(), ".claude"); | ||
| 84 | }); | ||
| 85 | |||
| 86 | /** | ||
| 87 | * Enumerate Claude Code skills from the user config dir and the workspace. | ||
| 88 | * Discovery is best-effort: unreadable roots and malformed skill entries are | ||
| 89 | * skipped so a broken skill never degrades the provider snapshot. On name | ||
| 90 | * collisions the project-scoped skill wins, matching Claude Code's | ||
| 91 | * most-specific-wins resolution. | ||
| 92 | */ | ||
| 93 | export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* ( | ||
| 94 | config: Pick<ClaudeSettings, "homePath">, | ||
| 95 | cwd?: string, | ||
| 96 | environment?: NodeJS.ProcessEnv, | ||
| 97 | ): Effect.fn.Return<ReadonlyArray<ServerProviderSkill>, never, FileSystem.FileSystem | Path.Path> { | ||
| 98 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 99 | const path = yield* Path.Path; | ||
| 100 | const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd); | ||
| 101 | |||
| 102 | const roots: ReadonlyArray<{ directory: string; scope: ClaudeSkillScope }> = [ | ||
| 103 | { directory: path.join(configDirPath, "skills"), scope: "user" }, | ||
| 104 | ...(cwd ? [{ directory: path.join(cwd, ".claude", "skills"), scope: "project" as const }] : []), | ||
| 105 | ]; | ||
| 106 | |||
| 107 | const skillsByName = new Map<string, ServerProviderSkill>(); | ||
| 108 | for (const root of roots) { | ||
| 109 | const entries = yield* fileSystem | ||
| 110 | .readDirectory(root.directory) | ||
| 111 | .pipe(Effect.orElseSucceed((): ReadonlyArray<string> => [])); | ||
| 112 | |||
| 113 | for (const entry of [...entries].sort()) { | ||
| 114 | const skillPath = path.join(root.directory, entry, "SKILL.md"); | ||
| 115 | const contents = yield* fileSystem | ||
| 116 | .readFileString(skillPath) | ||
| 117 | .pipe(Effect.orElseSucceed(() => undefined)); | ||
| 118 | if (contents === undefined) { | ||
| 119 | continue; | ||
| 120 | } | ||
| 121 | |||
| 122 | const frontmatter = parseSkillFrontmatter(contents); | ||
| 123 | // Malformed frontmatter means the skill won't load in Claude Code | ||
| 124 | // either — skip it rather than surfacing a broken entry under its | ||
| 125 | // directory name. | ||
| 126 | if (frontmatter.kind === "malformed") { | ||
| 127 | continue; | ||
| 128 | } | ||
| 129 | |||
| 130 | const name = (frontmatter.kind === "parsed" ? frontmatter.name : undefined) ?? entry.trim(); | ||
| 131 | if (!name) { | ||
| 132 | continue; | ||
| 133 | } | ||
| 134 | |||
| 135 | skillsByName.set(name, { | ||
| 136 | name, | ||
| 137 | path: skillPath, | ||
| 138 | enabled: true, | ||
| 139 | scope: root.scope, | ||
| 140 | ...(frontmatter.kind === "parsed" && frontmatter.description | ||
| 141 | ? { description: frontmatter.description } | ||
| 142 | : {}), | ||
| 143 | }); | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name)); | ||
| 148 | }); | ||
apps/server/src/provider/Layers/ClaudeProvider.ts+7-1| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 8 | import * as DateTime from "effect/DateTime"; | 8 | import * as DateTime from "effect/DateTime"; |
| 9 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as FileSystem from "effect/FileSystem"; | ||
| 10 | import * as Option from "effect/Option"; | 11 | import * as Option from "effect/Option"; |
| 11 | import * as Path from "effect/Path"; | 12 | import * as Path from "effect/Path"; |
| 12 | import * as Result from "effect/Result"; | 13 | import * as Result from "effect/Result"; |
| ... | @@ -40,6 +41,7 @@ import { | ... | @@ -40,6 +41,7 @@ import { |
| 40 | } from "../providerSnapshot.ts"; | 41 | } from "../providerSnapshot.ts"; |
| 41 | import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; | 42 | import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; |
| 42 | import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; | 43 | import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; |
| 44 | import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts"; | ||
| 43 | 45 | ||
| 44 | const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ | 46 | const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ |
| 45 | optionDescriptors: [], | 47 | optionDescriptors: [], |
| ... | @@ -732,10 +734,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( | ... | @@ -732,10 +734,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 732 | claudeSettings: ClaudeSettings, | 734 | claudeSettings: ClaudeSettings, |
| 733 | ) => Effect.Effect<ClaudeCapabilitiesProbe | undefined>, | 735 | ) => Effect.Effect<ClaudeCapabilitiesProbe | undefined>, |
| 734 | environment?: NodeJS.ProcessEnv, | 736 | environment?: NodeJS.ProcessEnv, |
| 737 | cwd?: string, | ||
| 735 | ): Effect.fn.Return< | 738 | ): Effect.fn.Return< |
| 736 | ServerProviderDraft, | 739 | ServerProviderDraft, |
| 737 | never, | 740 | never, |
| 738 | ChildProcessSpawner.ChildProcessSpawner | Path.Path | 741 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path |
| 739 | > { | 742 | > { |
| 740 | const resolvedEnvironment = environment ?? process.env; | 743 | const resolvedEnvironment = environment ?? process.env; |
| 741 | const checkedAt = DateTime.formatIso(yield* DateTime.now); | 744 | const checkedAt = DateTime.formatIso(yield* DateTime.now); |
| ... | @@ -845,6 +848,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( | ... | @@ -845,6 +848,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 845 | const capabilities = resolveCapabilities | 848 | const capabilities = resolveCapabilities |
| 846 | ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined)) | 849 | ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined)) |
| 847 | : undefined; | 850 | : undefined; |
| 851 | const skills = yield* discoverClaudeSkills(claudeSettings, cwd, resolvedEnvironment); | ||
| 848 | const slashCommands = capabilities?.slashCommands ?? []; | 852 | const slashCommands = capabilities?.slashCommands ?? []; |
| 849 | const dedupedSlashCommands = dedupeSlashCommands(slashCommands); | 853 | const dedupedSlashCommands = dedupeSlashCommands(slashCommands); |
| 850 | 854 | ||
| ... | @@ -855,6 +859,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( | ... | @@ -855,6 +859,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 855 | checkedAt, | 859 | checkedAt, |
| 856 | models, | 860 | models, |
| 857 | slashCommands: dedupedSlashCommands, | 861 | slashCommands: dedupedSlashCommands, |
| 862 | skills, | ||
| 858 | probe: { | 863 | probe: { |
| 859 | installed: true, | 864 | installed: true, |
| 860 | version: parsedVersion, | 865 | version: parsedVersion, |
| ... | @@ -876,6 +881,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( | ... | @@ -876,6 +881,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 876 | checkedAt, | 881 | checkedAt, |
| 877 | models, | 882 | models, |
| 878 | slashCommands: dedupedSlashCommands, | 883 | slashCommands: dedupedSlashCommands, |
| 884 | skills, | ||
| 879 | probe: { | 885 | probe: { |
| 880 | installed: true, | 886 | installed: true, |
| 881 | version: parsedVersion, | 887 | version: parsedVersion, |
pnpm-lock.yaml+3| ... | @@ -471,6 +471,9 @@ importers: | ... | @@ -471,6 +471,9 @@ importers: |
| 471 | node-pty: | 471 | node-pty: |
| 472 | specifier: ^1.1.0 | 472 | specifier: ^1.1.0 |
| 473 | version: 1.1.0 | 473 | version: 1.1.0 |
| 474 | yaml: | ||
| 475 | specifier: ^2.9.0 | ||
| 476 | version: 2.9.0 | ||
| 474 | devDependencies: | 477 | devDependencies: |
| 475 | '@effect/vitest': | 478 | '@effect/vitest': |
| 476 | specifier: 4.0.0-beta.78 | 479 | specifier: 4.0.0-beta.78 |