Skip to content

Commit 0432e09

Browse files
committed
feat: enable linking skills and subagents to slash commands with a new CLI command and gray-matter dependency
1 parent 00d4f91 commit 0432e09

5 files changed

Lines changed: 483 additions & 14 deletions

File tree

bin/cli.js

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import os from "os";
77
import { join } from "path";
88
import { loadConfig } from "../src/config.js";
99
import { trackDocs } from "../src/index.js";
10-
import {
10+
import {
1111
initClsync,
1212
stageItem,
1313
stageAll,
@@ -38,7 +38,10 @@ import {
3838
getClaudeDirsWithCache,
3939
getScanCacheInfo,
4040
clearScanCache,
41-
scanItems
41+
scanItems,
42+
linkSkillToCommand,
43+
linkSubagentToCommand,
44+
linkAll
4245
} from "../src/repo-sync.js";
4346

4447
// Get terminal width
@@ -1889,7 +1892,89 @@ program
18891892
console.log(chalk.dim(' Commands:'));
18901893
console.log(chalk.dim(' clsync promote <name> # project → user'));
18911894
console.log(chalk.dim(' clsync demote <name> # user → project\n'));
1892-
1895+
1896+
} catch (error) {
1897+
showError(error.message);
1898+
process.exit(1);
1899+
}
1900+
});
1901+
1902+
// ============================================================================
1903+
// LINK: Link skills/subagents to slash commands
1904+
// ============================================================================
1905+
program
1906+
.command("link [type] [name]")
1907+
.description("Link skills/subagents to slash commands")
1908+
.option("-u, --user", "User scope (default)", true)
1909+
.option("-p, --project", "Project scope")
1910+
.option("-a, --all", "Link all skills and agents")
1911+
.option("--skills-only", "Only link skills (with --all)")
1912+
.option("--agents-only", "Only link agents (with --all)")
1913+
.option("-n, --name <custom-name>", "Custom slash command name")
1914+
.action(async (type, name, options) => {
1915+
try {
1916+
const scope = options.project ? "project" : "user";
1917+
const spinner = ora();
1918+
1919+
// Link all
1920+
if (options.all) {
1921+
spinner.start("Linking all skills and agents...");
1922+
const results = await linkAll({
1923+
scope,
1924+
skillsOnly: options.skillsOnly,
1925+
agentsOnly: options.agentsOnly
1926+
});
1927+
spinner.succeed();
1928+
1929+
console.log(chalk.cyan("\n 📋 Linking Results:\n"));
1930+
if (results.skills.length > 0) {
1931+
console.log(chalk.bold(" Skills:"));
1932+
results.skills.forEach(r => {
1933+
console.log(chalk.dim(` ✓ ${r.skill} → /${r.command}`));
1934+
});
1935+
}
1936+
if (results.agents.length > 0) {
1937+
console.log(chalk.bold("\n Subagents:"));
1938+
results.agents.forEach(r => {
1939+
console.log(chalk.dim(` ✓ ${r.agent} → /${r.command}`));
1940+
});
1941+
}
1942+
console.log();
1943+
1944+
showSuccess('All links created!');
1945+
return;
1946+
}
1947+
1948+
// Validate arguments
1949+
if (!type || !name) {
1950+
showError("Usage: clsync link <skill|agent> <name>");
1951+
process.exit(1);
1952+
}
1953+
1954+
// Link single item
1955+
let result;
1956+
if (type === "skill") {
1957+
spinner.start(`Linking skill "${name}"...`);
1958+
result = await linkSkillToCommand(name, {
1959+
scope,
1960+
commandName: options.name
1961+
});
1962+
spinner.succeed();
1963+
console.log(chalk.dim(`\n ✓ Linked: ${result.skill} → /${result.command}\n`));
1964+
} else if (type === "agent") {
1965+
spinner.start(`Linking subagent "${name}"...`);
1966+
result = await linkSubagentToCommand(name, {
1967+
scope,
1968+
commandName: options.name
1969+
});
1970+
spinner.succeed();
1971+
console.log(chalk.dim(`\n ✓ Linked: ${result.agent} → /${result.command}\n`));
1972+
} else {
1973+
showError(`Unknown type "${type}". Use "skill" or "agent"`);
1974+
process.exit(1);
1975+
}
1976+
1977+
showSuccess('Link created!');
18931978
} catch (error) {
18941979
showError(error.message);
18951980
process.exit(1);

bin/mcp-server.js

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -601,18 +601,21 @@ server.registerTool(
601601
// Repository Sync Tools
602602
// =============================================================================
603603

604-
import {
605-
pullFromGitHub,
604+
import {
605+
pullFromGitHub,
606606
pushToGitHub,
607-
listLocalStaged,
608-
browseRepo,
607+
listLocalStaged,
608+
browseRepo,
609609
applyItem,
610610
applyAll,
611611
listRepoItems,
612612
listPulledRepos,
613613
promoteItem,
614614
demoteItem,
615-
listBothScopes
615+
listBothScopes,
616+
linkSkillToCommand,
617+
linkSubagentToCommand,
618+
linkAll
616619
} from "../src/repo-sync.js";
617620

618621
server.registerTool(
@@ -882,9 +885,9 @@ server.registerTool(
882885
async () => {
883886
try {
884887
const { project, user } = await listBothScopes();
885-
888+
886889
let text = "👁 Comparing Scopes\n\n";
887-
890+
888891
text += "📁 User (~/.claude):\n";
889892
if (user.length === 0) {
890893
text += " (empty)\n";
@@ -894,7 +897,7 @@ server.registerTool(
894897
text += ` ${icon} ${item.name}\n`;
895898
}
896899
}
897-
900+
898901
text += "\n📁 Project (.claude):\n";
899902
if (project.length === 0) {
900903
text += " (empty)\n";
@@ -904,7 +907,7 @@ server.registerTool(
904907
text += ` ${icon} ${item.name}\n`;
905908
}
906909
}
907-
910+
908911
text += "\nUse promote_setting or demote_setting to move items between scopes.";
909912
return { content: [{ type: "text", text }] };
910913
} catch (error) {
@@ -913,6 +916,113 @@ server.registerTool(
913916
}
914917
);
915918

919+
// =============================================================================
920+
// Link Tools - Connect skills/subagents to slash commands
921+
// =============================================================================
922+
923+
server.registerTool(
924+
"link_skill_to_command",
925+
{
926+
description: "Link a skill to a slash command for explicit invocation",
927+
inputSchema: {
928+
skillName: z.string().describe("Name of the skill to link"),
929+
commandName: z.string().optional().describe("Custom command name (defaults to skill name)"),
930+
scope: z.enum(["user", "project"]).optional().describe("Scope: user (~/.claude) or project (.claude). Default: user"),
931+
},
932+
},
933+
async ({ skillName, commandName, scope = "user" }) => {
934+
try {
935+
const result = await linkSkillToCommand(skillName, { scope, commandName });
936+
return {
937+
content: [{
938+
type: "text",
939+
text: `✅ Linked skill "${result.skill}" to command "/${result.command}"\n\nPath: ${result.path}\n\nYou can now use /${result.command} to explicitly invoke this skill.`
940+
}]
941+
};
942+
} catch (error) {
943+
return {
944+
content: [{ type: "text", text: `❌ Error: ${error.message}` }],
945+
isError: true
946+
};
947+
}
948+
}
949+
);
950+
951+
server.registerTool(
952+
"link_subagent_to_command",
953+
{
954+
description: "Link a subagent to a slash command for explicit invocation",
955+
inputSchema: {
956+
agentName: z.string().describe("Name of the subagent to link"),
957+
commandName: z.string().optional().describe("Custom command name (defaults to agent name)"),
958+
scope: z.enum(["user", "project"]).optional().describe("Scope: user (~/.claude) or project (.claude). Default: user"),
959+
},
960+
},
961+
async ({ agentName, commandName, scope = "user" }) => {
962+
try {
963+
const result = await linkSubagentToCommand(agentName, { scope, commandName });
964+
return {
965+
content: [{
966+
type: "text",
967+
text: `✅ Linked subagent "${result.agent}" to command "/${result.command}"\n\nPath: ${result.path}\n\nYou can now use /${result.command} to explicitly invoke this subagent.`
968+
}]
969+
};
970+
} catch (error) {
971+
return {
972+
content: [{ type: "text", text: `❌ Error: ${error.message}` }],
973+
isError: true
974+
};
975+
}
976+
}
977+
);
978+
979+
server.registerTool(
980+
"link_all_to_commands",
981+
{
982+
description: "Link all skills and subagents to slash commands",
983+
inputSchema: {
984+
scope: z.enum(["user", "project"]).optional().describe("Scope: user (~/.claude) or project (.claude). Default: user"),
985+
skillsOnly: z.boolean().optional().describe("Only link skills"),
986+
agentsOnly: z.boolean().optional().describe("Only link subagents"),
987+
},
988+
},
989+
async ({ scope = "user", skillsOnly, agentsOnly }) => {
990+
try {
991+
const results = await linkAll({ scope, skillsOnly, agentsOnly });
992+
993+
let message = "✅ Linked all items to slash commands\n\n";
994+
995+
if (results.skills.length > 0) {
996+
message += "**Skills:**\n";
997+
results.skills.forEach(r => {
998+
message += `- ${r.skill} → /${r.command}\n`;
999+
});
1000+
message += "\n";
1001+
}
1002+
1003+
if (results.agents.length > 0) {
1004+
message += "**Subagents:**\n";
1005+
results.agents.forEach(r => {
1006+
message += `- ${r.agent} → /${r.command}\n`;
1007+
});
1008+
}
1009+
1010+
if (results.skills.length === 0 && results.agents.length === 0) {
1011+
message += "No skills or subagents found to link.";
1012+
}
1013+
1014+
return {
1015+
content: [{ type: "text", text: message }]
1016+
};
1017+
} catch (error) {
1018+
return {
1019+
content: [{ type: "text", text: `❌ Error: ${error.message}` }],
1020+
isError: true
1021+
};
1022+
}
1023+
}
1024+
);
1025+
9161026
// =============================================================================
9171027
// Resource Registration
9181028
// =============================================================================

0 commit comments

Comments
 (0)