Skip to content

Commit c679f98

Browse files
committed
fix(server): discover Claude skills from add-dir launch args
1 parent 09c5e7a commit c679f98

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

‎apps/server/src/provider/Drivers/ClaudeSkills.test.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as FileSystem from "effect/FileSystem";
66
import * as Path from "effect/Path";
77

88
import { discoverClaudeSkills, skillOverrideSettingsPaths } from "./ClaudeSkills.ts";
9+
import { planClaudeSkillDispatch } from "./ClaudeSkillDispatch.ts";
910

1011
const writeSkill = Effect.fn(function* (
1112
skillsDir: string,
@@ -67,6 +68,65 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
6768
}),
6869
);
6970

71+
it.effect("discovers skills from the instance's --add-dir launch args", () =>
72+
Effect.gen(function* () {
73+
const fs = yield* FileSystem.FileSystem;
74+
const path = yield* Path.Path;
75+
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" });
76+
const configDir = path.join(tempDir, "claude-home");
77+
const workspace = path.join(tempDir, "workspace");
78+
const extraDir = path.join(workspace, "extra skills");
79+
yield* fs.makeDirectory(workspace, { recursive: true });
80+
yield* writeSkill(
81+
path.join(extraDir, ".claude", "skills"),
82+
"demo",
83+
["---", "description: From an additional directory.", "---"].join("\n"),
84+
);
85+
86+
assert.deepEqual(yield* discoverClaudeSkills({ homePath: configDir }, workspace), []);
87+
for (const launchArgs of [
88+
`--add-dir "${extraDir}"`,
89+
`--add-dir="${extraDir}"`,
90+
'--add-dir "extra skills"',
91+
'--add-dir="extra skills"',
92+
`--add-dir missing --add-dir "${extraDir}"`,
93+
]) {
94+
const settings = { homePath: configDir, launchArgs };
95+
const skills = yield* discoverClaudeSkills(settings, workspace);
96+
assert.deepEqual(skills, [
97+
{
98+
name: "demo",
99+
path: path.join(extraDir, ".claude", "skills", "demo", "SKILL.md"),
100+
enabled: true,
101+
scope: "project",
102+
description: "From an additional directory.",
103+
},
104+
]);
105+
}
106+
const settings = { homePath: configDir, launchArgs: `--add-dir "${extraDir}"` };
107+
const skills = yield* discoverClaudeSkills(settings, workspace);
108+
assert.deepEqual(
109+
planClaudeSkillDispatch("$demo", new Set(skills.map((skill) => skill.name))),
110+
{
111+
leadingText: undefined,
112+
commandText: "/demo",
113+
skillName: "demo",
114+
},
115+
);
116+
yield* writeSkill(
117+
path.join(workspace, ".claude", "skills"),
118+
"demo",
119+
"---\ndescription: Project copy.\n---",
120+
);
121+
const projectSkills = yield* discoverClaudeSkills(settings, workspace);
122+
assert.equal(projectSkills[0]?.description, "Project copy.");
123+
for (const launchArgs of ["--add-dir", '--add-dir=""', '--add-dir "extra skills"']) {
124+
const settings = { homePath: configDir, launchArgs };
125+
assert.deepEqual(yield* discoverClaudeSkills(settings), []);
126+
}
127+
}),
128+
);
129+
70130
it.effect("ignores .agents/skills, which Claude Code does not load", () =>
71131
Effect.gen(function* () {
72132
const fs = yield* FileSystem.FileSystem;

‎apps/server/src/provider/Drivers/ClaudeSkills.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as FileSystem from "effect/FileSystem";
44
import * as Path from "effect/Path";
55
import * as Schema from "effect/Schema";
66
import * as HostProcess from "@t3tools/shared/HostProcess";
7+
import { parseCliArgs } from "@t3tools/shared/cliArgs";
78
import { fromLenientJson } from "@t3tools/shared/schemaJson";
89
import { parse as parseYamlDocument } from "yaml";
910

@@ -267,7 +268,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct
267268
* both scopes, the user copy is the one that runs.
268269
*/
269270
export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (
270-
config: Pick<ClaudeSettings, "homePath">,
271+
config: Pick<ClaudeSettings, "homePath"> & Partial<Pick<ClaudeSettings, "launchArgs">>,
271272
cwd?: string,
272273
environment?: NodeJS.ProcessEnv,
273274
): Effect.fn.Return<ReadonlyArray<ServerProviderSkill>, never, FileSystem.FileSystem | Path.Path> {
@@ -276,9 +277,23 @@ export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function*
276277
const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
277278
const skillOverrides = yield* readSkillOverrides(configDirPath, cwd, environment ?? process.env);
278279

280+
// Use the session's argument parser so discovery sees the same --add-dir value.
281+
const additionalDirectory = parseCliArgs(config.launchArgs ?? "").flags["add-dir"];
279282
const roots: ReadonlyArray<{ directory: string; scope: ClaudeSkillScope }> = [
280283
{ directory: path.join(configDirPath, "skills"), scope: "user" },
281284
...(cwd ? [{ directory: path.join(cwd, ".claude", "skills"), scope: "project" as const }] : []),
285+
...(additionalDirectory && (cwd || path.isAbsolute(additionalDirectory))
286+
? [
287+
{
288+
directory: path.join(
289+
path.resolve(cwd ?? ".", additionalDirectory),
290+
".claude",
291+
"skills",
292+
),
293+
scope: "project" as const,
294+
},
295+
]
296+
: []),
282297
];
283298

284299
const skillsByName = new Map<string, ServerProviderSkill>();

0 commit comments

Comments
 (0)