Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/required-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": patch
---

Install the full Clerk core and feature skill sets by default during `clerk init`. Agents now get context for `clerk-custom-ui`, `clerk-backend-api`, `clerk-orgs`, `clerk-testing`, and `clerk-webhooks` in addition to the previous defaults, plus a framework-specific skill when one matches. Pass `--no-skills` to opt out.
15 changes: 10 additions & 5 deletions packages/cli-core/src/commands/init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Use `--prompt` to output a setup prompt for an AI agent without running init.
14. Prints a summary of created, modified, and skipped files with recommendations
15. **Authenticated mode**: pulls development instance API keys via `clerk env pull`
16. **Unauthenticated mode**: prints instructions for development without API keys and how to connect a Clerk account later
17. Optionally installs framework-specific Clerk agent skills via the project's package runner (see [Agent skills install](#agent-skills-install))
17. Optionally installs Clerk agent skills (core + features, plus a framework-specific skill) via the project's package runner (see [Agent skills install](#agent-skills-install))

## Framework Detection

Expand Down Expand Up @@ -197,9 +197,14 @@ At install time, [`skills.ts`](./skills.ts) stages the bundled content into a fr

The `skills` CLI writes the installed files into each agent's skill directory (`.claude/skills/clerk/`, `.cursor/skills/clerk/`, etc.) and records the entry in the project's `skills-lock.json` with `sourceType: "local"`, which correctly excludes it from `skills update` (the skill can only change when the CLI itself is upgraded).

### 2. The upstream framework-pattern skills
### 2. The upstream skills

The base skills `clerk` and `clerk-setup` are always included from [`clerk/skills`](https://github.com/clerk/skills). The detected framework dependency adds a matching skill:
A fixed default set is always installed from [`clerk/skills`](https://github.com/clerk/skills), covering the `core/` and `features/` directories:

- **Core**: `clerk-setup`, `clerk-custom-ui`, `clerk-backend-api`
- **Features**: `clerk-orgs`, `clerk-testing`, `clerk-webhooks`

The detected framework dependency adds one more skill on top:

| Framework dep | Added skill |
| ----------------------- | ----------------------------- |
Expand All @@ -211,8 +216,8 @@ The base skills `clerk` and `clerk-setup` are always included from [`clerk/skill
| `astro` | `clerk-astro-patterns` |
| `@tanstack/react-start` | `clerk-tanstack-patterns` |
| `expo` | `clerk-expo-patterns` |
| `express` | `clerk-backend-api` |
| `fastify` | `clerk-backend-api` |

Express and Fastify projects don't get a framework-specific skill — `clerk-backend-api` (now a default) already covers their needs.

These skills version independently of the CLI, so no pin is applied.

Expand Down
47 changes: 47 additions & 0 deletions packages/cli-core/src/commands/init/skills.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test, expect, describe } from "bun:test";
import { formatSkillsPromptMessage, resolveUpstreamSkills } from "./skills.ts";

const DEFAULTS = [
"clerk-setup",
"clerk-custom-ui",
"clerk-backend-api",
"clerk-orgs",
"clerk-testing",
"clerk-webhooks",
];

describe("resolveUpstreamSkills", () => {
test("returns the 6 defaults when no framework is detected", () => {
expect(resolveUpstreamSkills(undefined)).toEqual(DEFAULTS);
});

test("appends the framework skill for a known dep", () => {
expect(resolveUpstreamSkills("next")).toEqual([...DEFAULTS, "clerk-nextjs-patterns"]);
});

test("returns just the defaults for express (clerk-backend-api is already a default)", () => {
expect(resolveUpstreamSkills("express")).toEqual(DEFAULTS);
});

test("returns just the defaults for fastify (clerk-backend-api is already a default)", () => {
expect(resolveUpstreamSkills("fastify")).toEqual(DEFAULTS);
});

test("returns just the defaults for an unknown framework dep", () => {
expect(resolveUpstreamSkills("svelte")).toEqual(DEFAULTS);
});
});

describe("formatSkillsPromptMessage", () => {
test("summarizes without a framework skill", () => {
expect(formatSkillsPromptMessage(undefined)).toBe(
"Install agent skills? (clerk core + features)",
);
});

test("strips the clerk- prefix from the framework skill", () => {
expect(formatSkillsPromptMessage("clerk-nextjs-patterns")).toBe(
"Install agent skills? (clerk core + features + nextjs-patterns)",
);
});
});
61 changes: 46 additions & 15 deletions packages/cli-core/src/commands/init/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* imports). Delegated to the `clerk skill install` core helpers in
* `commands/skill/install.ts`.
*
* 2. The framework-pattern skills (`clerk-setup`,
* `clerk-<framework>-patterns`) ship from the upstream `clerk/skills`
* repo and version independently of the CLI.
* 2. The upstream skills (`clerk-setup`, `clerk-custom-ui`,
* `clerk-backend-api`, `clerk-orgs`, `clerk-testing`, `clerk-webhooks`,
* plus a framework-specific skill when one matches) ship from the
* upstream `clerk/skills` repo and version independently of the CLI.
*
* The skills CLI itself handles agent auto-detection and scope selection:
* in interactive mode we hand off entirely (no `--agent` / `-y`), so the
Expand All @@ -23,10 +24,19 @@ import { confirm } from "../../lib/prompts.js";
import type { ProjectContext } from "./frameworks/types.js";
import { installClerkSkillCore, resolveSkillsRunner, runSkillsAdd } from "../skill/install.js";

/** Upstream skills installed regardless of framework. */
const BASE_SKILLS = ["clerk-setup"];
/** Upstream skills from clerk/skills — installed on every project (excludes the bundled clerk skill). */
const DEFAULT_UPSTREAM_SKILLS = [
// core/
"clerk-setup",
"clerk-custom-ui",
"clerk-backend-api",
// features/
"clerk-orgs",
"clerk-testing",
"clerk-webhooks",
];

/** Maps framework dep (from package.json) to the upstream skill name. */
// Express/Fastify have no entry — their skill is clerk-backend-api, which is a default.
Comment thread
rafa-thayto marked this conversation as resolved.
const FRAMEWORK_SKILL_MAP: Record<string, string> = {
next: "clerk-nextjs-patterns",
react: "clerk-react-patterns",
Expand All @@ -36,19 +46,39 @@ const FRAMEWORK_SKILL_MAP: Record<string, string> = {
astro: "clerk-astro-patterns",
"@tanstack/react-start": "clerk-tanstack-patterns",
expo: "clerk-expo-patterns",
express: "clerk-backend-api",
fastify: "clerk-backend-api",
};

// Guard against accidental overlap: Set.add() silently deduplicates, masking dead entries.
for (const [dep, skill] of Object.entries(FRAMEWORK_SKILL_MAP)) {
if (DEFAULT_UPSTREAM_SKILLS.includes(skill)) {
throw new Error(
`FRAMEWORK_SKILL_MAP['${dep}'] maps to '${skill}', which is already in DEFAULT_UPSTREAM_SKILLS. Remove it from the map.`,
);
}
}

/** Source for upstream framework-pattern skills. */
const UPSTREAM_SKILLS_SOURCE = "clerk/skills";

function resolveUpstreamSkills(frameworkDep: string | undefined): string[] {
const skills = [...BASE_SKILLS];
export function resolveUpstreamSkills(frameworkDep: string | undefined): string[] {
const skills = new Set(DEFAULT_UPSTREAM_SKILLS);
if (frameworkDep && FRAMEWORK_SKILL_MAP[frameworkDep]) {
skills.push(FRAMEWORK_SKILL_MAP[frameworkDep]);
skills.add(FRAMEWORK_SKILL_MAP[frameworkDep]);
}
return skills;
return [...skills];
}

export function getFrameworkSkill(frameworkDep: string | undefined): string | undefined {
return frameworkDep ? FRAMEWORK_SKILL_MAP[frameworkDep] : undefined;
}

function formatSkillsSummary(frameworkSkill: string | undefined): string {
const framework = frameworkSkill ? ` + ${frameworkSkill.replace(/^clerk-/, "")}` : "";
return `clerk core + features${framework}`;
}

export function formatSkillsPromptMessage(frameworkSkill: string | undefined): string {
return `Install agent skills? (${formatSkillsSummary(frameworkSkill)})`;
}

export async function installSkills(
Expand All @@ -58,11 +88,11 @@ export async function installSkills(
skipPrompt: boolean,
): Promise<void> {
const upstreamSkills = resolveUpstreamSkills(frameworkDep);
const skillList = ["clerk", ...upstreamSkills].join(", ");
const frameworkSkill = getFrameworkSkill(frameworkDep);

if (isHuman() && !skipPrompt) {
const install = await confirm({
message: `Install agent skills? (${skillList})`,
message: formatSkillsPromptMessage(frameworkSkill),
default: true,
});
if (!install) return;
Expand All @@ -79,14 +109,15 @@ export async function installSkills(
// problem with one source doesn't block the other.
const cliSkillOk = await installClerkSkillCore(runner, cwd, interactive);

log.debug(`skills: upstream install — ${upstreamSkills.join(", ")}`);
const upstreamOk = await runSkillsAdd(
runner,
cwd,
UPSTREAM_SKILLS_SOURCE,
upstreamSkills,
interactive,
false,
upstreamSkills.join(", "),
formatSkillsSummary(frameworkSkill),
);

if (cliSkillOk && upstreamOk) {
Expand Down
Loading