| 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 | 31 | "@opencode-ai/sdk": "^1.3.15", |
| 32 | 32 | "@pierre/diffs": "catalog:", |
| 33 | 33 | "effect": "catalog:", |
| 34 | "node-pty": "^1.1.0" | |
| 34 | "node-pty": "^1.1.0", | |
| 35 | "yaml": "catalog:" | |
| 35 | 36 | }, |
| 36 | 37 | "devDependencies": { |
| 37 | 38 | "@effect/vitest": "catalog:", |
apps/server/src/provider/Drivers/ClaudeDriver.ts+3| ... | ... | @@ -118,6 +118,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 118 | 118 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 119 | 119 | Effect.gen(function* () { |
| 120 | 120 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 121 | const fileSystem = yield* FileSystem.FileSystem; | |
| 121 | 122 | const path = yield* Path.Path; |
| 122 | 123 | const { cwd } = yield* ServerConfig; |
| 123 | 124 | const httpClient = yield* HttpClient.HttpClient; |
| ... | ... | @@ -165,9 +166,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 165 | 166 | effectiveConfig, |
| 166 | 167 | () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), |
| 167 | 168 | processEnv, |
| 169 | cwd, | |
| 168 | 170 | ).pipe( |
| 169 | 171 | Effect.map(stampIdentity), |
| 170 | 172 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 173 | Effect.provideService(FileSystem.FileSystem, fileSystem), | |
| 171 | 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 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import * as DateTime from "effect/DateTime"; |
| 9 | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as FileSystem from "effect/FileSystem"; | |
| 10 | 11 | import * as Option from "effect/Option"; |
| 11 | 12 | import * as Path from "effect/Path"; |
| 12 | 13 | import * as Result from "effect/Result"; |
| ... | ... | @@ -40,6 +41,7 @@ import { |
| 40 | 41 | } from "../providerSnapshot.ts"; |
| 41 | 42 | import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; |
| 42 | 43 | import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; |
| 44 | import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts"; | |
| 43 | 45 | |
| 44 | 46 | const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ |
| 45 | 47 | optionDescriptors: [], |
| ... | ... | @@ -732,10 +734,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 732 | 734 | claudeSettings: ClaudeSettings, |
| 733 | 735 | ) => Effect.Effect<ClaudeCapabilitiesProbe | undefined>, |
| 734 | 736 | environment?: NodeJS.ProcessEnv, |
| 737 | cwd?: string, | |
| 735 | 738 | ): Effect.fn.Return< |
| 736 | 739 | ServerProviderDraft, |
| 737 | 740 | never, |
| 738 | ChildProcessSpawner.ChildProcessSpawner | Path.Path | |
| 741 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | |
| 739 | 742 | > { |
| 740 | 743 | const resolvedEnvironment = environment ?? process.env; |
| 741 | 744 | const checkedAt = DateTime.formatIso(yield* DateTime.now); |
| ... | ... | @@ -845,6 +848,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 845 | 848 | const capabilities = resolveCapabilities |
| 846 | 849 | ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined)) |
| 847 | 850 | : undefined; |
| 851 | const skills = yield* discoverClaudeSkills(claudeSettings, cwd, resolvedEnvironment); | |
| 848 | 852 | const slashCommands = capabilities?.slashCommands ?? []; |
| 849 | 853 | const dedupedSlashCommands = dedupeSlashCommands(slashCommands); |
| 850 | 854 | |
| ... | ... | @@ -855,6 +859,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 855 | 859 | checkedAt, |
| 856 | 860 | models, |
| 857 | 861 | slashCommands: dedupedSlashCommands, |
| 862 | skills, | |
| 858 | 863 | probe: { |
| 859 | 864 | installed: true, |
| 860 | 865 | version: parsedVersion, |
| ... | ... | @@ -876,6 +881,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 876 | 881 | checkedAt, |
| 877 | 882 | models, |
| 878 | 883 | slashCommands: dedupedSlashCommands, |
| 884 | skills, | |
| 879 | 885 | probe: { |
| 880 | 886 | installed: true, |
| 881 | 887 | version: parsedVersion, |
pnpm-lock.yaml+3| ... | ... | @@ -471,6 +471,9 @@ importers: |
| 471 | 471 | node-pty: |
| 472 | 472 | specifier: ^1.1.0 |
| 473 | 473 | version: 1.1.0 |
| 474 | yaml: | |
| 475 | specifier: ^2.9.0 | |
| 476 | version: 2.9.0 | |
| 474 | 477 | devDependencies: |
| 475 | 478 | '@effect/vitest': |
| 476 | 479 | specifier: 4.0.0-beta.78 |