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/rename-clerk-cli-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Rename the bundled agent skill from `clerk` to `clerk-cli` for more clarity during install. After upgrading, `clerk skill install` (and the install step in `clerk init`) writes the skill to `<agent-dir>/skills/clerk-cli/` instead of `<agent-dir>/skills/clerk/`. Existing `skills/clerk/` directories from prior installs are left in place; remove them manually if you want to avoid duplicate context.
20 changes: 10 additions & 10 deletions .claude/skills/audit-clerk-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
---
name: audit-clerk-skill
description: Audits the Clerk CLI source tree and proposes updates to the bundled `clerk` skill so it stays in sync with the binary. Use when the user says "audit the clerk skill", "update the clerk skill", "check the skill against the code", "resync clerk skill", or after adding/renaming/removing CLI commands, flags, or agent-mode behavior.
description: Audits the Clerk CLI source tree and proposes updates to the bundled `clerk-cli` skill so it stays in sync with the binary. Use when the user says "audit the clerk-cli skill", "update the clerk-cli skill", "check the skill against the code", "resync clerk-cli skill", or after adding/renaming/removing CLI commands, flags, or agent-mode behavior.
effort: high
user-invocable: true
disable-model-invocation: true
argument-hint: "[--apply]"
---

# Audit the clerk Skill
# Audit the clerk-cli Skill

Cross-check `skills/clerk/` against the actual CLI source in `packages/cli-core/` and propose precise edits wherever they have drifted. The binary is the source of truth; the skill is documentation that must track it.
Cross-check `skills/clerk-cli/` against the actual CLI source in `packages/cli-core/` and propose precise edits wherever they have drifted. The binary is the source of truth; the skill is documentation that must track it.

**ultrathink** on this task. It requires building a full command tree, comparing two representations of it, and making judgment calls about what belongs in the skill vs. in `references/*.md` vs. in `--help`. Shallow passes miss drift.

## Inputs

- **Source of truth**: `packages/cli-core/src/commands/**` (one directory per top-level command), plus `packages/cli-core/src/cli.ts`, `cli-program.ts`, `mode.ts`, and anything in `packages/cli-core/src/lib/` referenced by commands (runner preference, agent mode, doctor checks, key resolution).
- **Target**: `skills/clerk/SKILL.md` and `skills/clerk/references/*.md`.
- **Target**: `skills/clerk-cli/SKILL.md` and `skills/clerk-cli/references/*.md`.
- **Template markers**: the skill uses `{{CLI_VERSION}}` placeholders substituted at install time by `clerk skill install`. Preserve them; do not expand.

## Workflow
Expand All @@ -37,7 +37,7 @@ Read the per-command `README.md` (`packages/cli-core/src/commands/<name>/README.
- Flag commands or sub-paths marked mocked/stubbed (blockquote at top of the README). The skill should not document these as production-ready.
- Cross-check Clerk API endpoint claims in `references/recipes.md`.

Do **not** propose bundling the READMEs into `skills/clerk/references/` (symlinks or text imports). They ship internal detail agents do not need, and inflate the compiled binary. They are a reference for the audit, not for the skill.
Do **not** propose bundling the READMEs into `skills/clerk-cli/references/` (symlinks or text imports). They ship internal detail agents do not need, and inflate the compiled binary. They are a reference for the audit, not for the skill.

Also capture cross-cutting behavior:

Expand All @@ -50,7 +50,7 @@ Don't memorize output. Prefer reading the source directly over running the binar

### 2. Extract the skill's current claims

Read `skills/clerk/SKILL.md` and each file under `skills/clerk/references/`. Extract every concrete claim:
Read `skills/clerk-cli/SKILL.md` and each file under `skills/clerk-cli/references/`. Extract every concrete claim:

- Every command mentioned in the "Core commands at a glance" table and the Invoking-the-CLI table.
- Every flag called out by name.
Expand Down Expand Up @@ -92,7 +92,7 @@ If a new reference file is warranted (e.g. a `references/commands.md` table), pr

Emit a review-ready proposal. For each change:

- Path (`skills/clerk/SKILL.md` or `skills/clerk/references/<file>.md`).
- Path (`skills/clerk-cli/SKILL.md` or `skills/clerk-cli/references/<file>.md`).
- Why (source citation: `packages/cli-core/src/commands/<cmd>/<file>.ts:<line>`).
- Either a unified diff (preferred) or a before/after block for prose sections.
- Severity: `drift` (factually wrong today), `gap` (missing coverage), `polish` (clearer wording, better placement, or cuts that route the agent to `--help` instead of duplicating it).
Expand All @@ -118,18 +118,18 @@ If invoked as `/audit-clerk-skill --apply`, apply `drift` and `gap` edits direct
Return the proposal as:

```
# clerk skill audit — <YYYY-MM-DD>
# clerk-cli skill audit — <YYYY-MM-DD>
Comment thread
kylemac marked this conversation as resolved.

## Summary
<counts per bucket, one-line headline of the biggest drift>

## skills/clerk/SKILL.md
## skills/clerk-cli/SKILL.md
### <section name>
- [drift|gap|polish] <one-line description>
- source: packages/cli-core/src/commands/<...>:<line>
- <diff or before/after>

## skills/clerk/references/<file>.md
## skills/clerk-cli/references/<file>.md
...

## New files (if any)
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,15 +257,15 @@ describe("help: clerk skill install tip", () => {
});

for (const dir of STANDARD_AGENT_DIRS) {
test(`hides the tip when ${dir}/skills/clerk/SKILL.md exists under HOME`, () => {
const target = join(tmpHome, dir, "skills/clerk");
test(`hides the tip when ${dir}/skills/clerk-cli/SKILL.md exists under HOME`, () => {
const target = join(tmpHome, dir, "skills/clerk-cli");
mkdirSync(target, { recursive: true });
writeFileSync(join(target, "SKILL.md"), "ok");
expect(renderHelp()).not.toContain(TIP_SUBSTR);
});

test(`hides the tip when ${dir}/skills/clerk/SKILL.md exists under cwd`, () => {
const target = join(tmpCwd, dir, "skills/clerk");
test(`hides the tip when ${dir}/skills/clerk-cli/SKILL.md exists under cwd`, () => {
const target = join(tmpCwd, dir, "skills/clerk-cli");
mkdirSync(target, { recursive: true });
writeFileSync(join(target, "SKILL.md"), "ok");
expect(renderHelp()).not.toContain(TIP_SUBSTR);
Expand Down
10 changes: 5 additions & 5 deletions packages/cli-core/src/commands/init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,17 @@ After scaffolding (and after env keys are pulled or keyless instructions are pri

- **Human mode**: prompts `Install agent skills? (...)` defaulting to yes. Pass `--no-skills` to suppress the prompt entirely, or `-y/--yes` to accept it without confirmation. When more than one runner is available, a second prompt picks which one to use (the project's package manager wins by default).
- **Agent mode**: skills are installed non-interactively with `-y -g` flags (no prompt shown). Pass `--no-skills` to skip entirely.
- **`--prompt`**: exits before the skills step runs. Agent users should run `skills add clerk/skills` via their preferred runner manually; the bundled `clerk` skill is only installable via `clerk init` itself, since its source lives inside the CLI binary.
- **`--prompt`**: exits before the skills step runs. Agent users should run `skills add clerk/skills` via their preferred runner manually; the bundled `clerk-cli` skill is only installable via `clerk init` itself, since its source lives inside the CLI binary.

Two install commands run, sharing one runner:

### 1. The bundled `clerk` skill
### 1. The bundled `clerk-cli` skill

The `clerk` skill ships **inside the CLI binary**. Its markdown files at [`<repo-root>/skills/clerk/`](../../../../../skills/clerk/) are pulled into [`skills.ts`](./skills.ts) as [text imports](https://bun.com/docs/bundler/loaders#text) (`import md from "./SKILL.md" with { type: "text" }`) and embedded by `bun build --compile`, so the skill content always matches the binary running it. No network, no tag, no version fallback.
The `clerk-cli` skill ships **inside the CLI binary**. Its markdown files at [`<repo-root>/skills/clerk-cli/`](../../../../../skills/clerk-cli/) are pulled into [`skills.ts`](./skills.ts) as [text imports](https://bun.com/docs/bundler/loaders#text) (`import md from "./SKILL.md" with { type: "text" }`) and embedded by `bun build --compile`, so the skill content always matches the binary running it. No network, no tag, no version fallback.

At install time, [`skills.ts`](./skills.ts) stages the bundled content into a fresh temp directory (`mkdtemp`) and invokes `<runner> skills add <tmpdir> --copy`. The `--copy` flag is required: the default symlink mode would point each agent's skill dir at the temp dir, which we delete immediately after the install completes.

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).
The `skills` CLI writes the installed files into each agent's skill directory (`.claude/skills/clerk-cli/`, `.cursor/skills/clerk-cli/`, 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 skills

Expand Down Expand Up @@ -223,7 +223,7 @@ These skills version independently of the CLI, so no pin is applied.

### Failure handling

The two install commands fail independently: a problem with the bundled `clerk` skill install (e.g. the `skills` CLI can't be fetched by the runner) does not block the upstream skills install, and vice versa. Each failure prints its own yellow warning with a manual install command (where applicable — the bundled `clerk` skill has no standalone manual command, since its source lives in the binary). Init continues and exits successfully either way.
The two install commands fail independently: a problem with the bundled `clerk-cli` skill install (e.g. the `skills` CLI can't be fetched by the runner) does not block the upstream skills install, and vice versa. Each failure prints its own yellow warning with a manual install command (where applicable — the bundled `clerk-cli` skill has no standalone manual command, since its source lives in the binary). Init continues and exits successfully either way.

Implementation lives in [`skills.ts`](./skills.ts). Note that the E2E fixture setup runs `clerk init --yes --no-skills` because the framework template skills reference auto-generated types (e.g. React Router's `./+types/root`) that don't exist outside a real app directory and would break the fixture's `tsc` step.

Expand Down
6 changes: 4 additions & 2 deletions packages/cli-core/src/commands/skill/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Skill Command

Manages the bundled `clerk` agent skill. The skill is embedded in the CLI binary at compile time via text imports from `skills/clerk/`, so it always matches the version of the CLI in use.
Manages the bundled `clerk-cli` agent skill. The skill is embedded in the CLI binary at compile time via text imports from `skills/clerk-cli/`, so it always matches the version of the CLI in use.

## Subcommands

### `clerk skill install`

Installs the bundled `clerk` skill for any locally detected AI agents (Claude Code, Cursor, etc.). The actual agent detection and scope selection is delegated to the external [`skills`](https://www.npmjs.com/package/skills) CLI, which is invoked via the preferred package runner on PATH (`bunx`, `pnpm dlx`, `yarn dlx`, or `npx`).
Installs the bundled `clerk-cli` skill for any locally detected AI agents (Claude Code, Cursor, etc.). The actual agent detection and scope selection is delegated to the external [`skills`](https://www.npmjs.com/package/skills) CLI, which is invoked via the preferred package runner on PATH (`bunx`, `pnpm dlx`, `yarn dlx`, or `npx`).

Interactive mode hands off entirely to the `skills` CLI picker. Non-interactive mode (`-y`, agent mode, or no TTY) passes `-y -g` so the skills CLI runs unattended against global scope with auto-detected agents.

This command is delegated to by `clerk init` as part of its post-scaffold agent skills step; running it standalone is useful when adding the skill to an existing project that was set up before the skill was bundled, or when reinstalling after upgrading the CLI.

> Prior to v1, the bundled skill was named `clerk`. It is now `clerk-cli` to make the name unambiguous during install. If you previously installed the `clerk` skill, you can remove the old `skills/clerk/` directory under any agent dir (`.claude/`, `.cursor/`, etc.) — the new install lives at `skills/clerk-cli/`.
## Usage

```sh
Expand Down
37 changes: 20 additions & 17 deletions packages/cli-core/src/commands/skill/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ describe("buildSkillsArgs", () => {
expect(buildSkillsArgs(upstream, skills, false, false)).not.toContain("--agent");
});

test("empty skillNames omits --skill flags (used for the clerk source)", () => {
const stageDir = "/tmp/clerk-skill-abc";
test("empty skillNames omits --skill flags (used for the clerk-cli source)", () => {
const stageDir = "/tmp/clerk-cli-skill-abc";
const args = buildSkillsArgs(stageDir, [], true, true);
expect(args).toEqual(["skills", "add", stageDir, "--copy"]);
expect(args).not.toContain("--skill");
});

test("copy=true appends --copy flag (required for the staged clerk dir)", () => {
const args = buildSkillsArgs("/tmp/clerk-skill-xyz", [], false, true);
test("copy=true appends --copy flag (required for the staged clerk-cli dir)", () => {
const args = buildSkillsArgs("/tmp/clerk-cli-skill-xyz", [], false, true);
expect(args).toContain("--copy");
// --copy should trail -y / -g, not replace them.
expect(args).toContain("-y");
Expand All @@ -62,20 +62,23 @@ describe("withStagedClerkSkill", () => {

await withStagedClerkSkill(undefined, async (dir) => {
const files = {
"clerk/SKILL.md": await readFile(join(dir, "clerk/SKILL.md"), "utf-8"),
"clerk/references/auth.md": await readFile(join(dir, "clerk/references/auth.md"), "utf-8"),
"clerk/references/recipes.md": await readFile(
join(dir, "clerk/references/recipes.md"),
"clerk-cli/SKILL.md": await readFile(join(dir, "clerk-cli/SKILL.md"), "utf-8"),
"clerk-cli/references/auth.md": await readFile(
join(dir, "clerk-cli/references/auth.md"),
"utf-8",
),
"clerk/references/agent-mode.md": await readFile(
join(dir, "clerk/references/agent-mode.md"),
"clerk-cli/references/recipes.md": await readFile(
join(dir, "clerk-cli/references/recipes.md"),
"utf-8",
),
"clerk-cli/references/agent-mode.md": await readFile(
join(dir, "clerk-cli/references/agent-mode.md"),
"utf-8",
),
};
observed = { dir, files };

const entry = await stat(join(dir, "clerk"));
const entry = await stat(join(dir, "clerk-cli"));
expect(entry.isDirectory()).toBe(true);
});

Expand All @@ -89,7 +92,7 @@ describe("withStagedClerkSkill", () => {
}

// SKILL.md should at least contain the YAML frontmatter marker.
expect(files["clerk/SKILL.md"]).toContain("---");
expect(files["clerk-cli/SKILL.md"]).toContain("---");

// Temp dir is removed once the callback returns.
expect(existsSync(dir)).toBe(false);
Expand All @@ -111,10 +114,10 @@ describe("withStagedClerkSkill", () => {
});

const ALL_BUNDLED_FILES = [
"clerk/SKILL.md",
"clerk/references/auth.md",
"clerk/references/recipes.md",
"clerk/references/agent-mode.md",
"clerk-cli/SKILL.md",
"clerk-cli/references/auth.md",
"clerk-cli/references/recipes.md",
"clerk-cli/references/agent-mode.md",
] as const;

describe("withStagedClerkSkill version rendering", () => {
Expand Down Expand Up @@ -145,7 +148,7 @@ describe("bundled SKILL.md frontmatter", () => {
// `clerk skill install` fail with "No valid skills found".
test("parses as YAML with name and description strings", async () => {
await withStagedClerkSkill(undefined, async (stageDir) => {
const content = await readFile(join(stageDir, "clerk/SKILL.md"), "utf8");
const content = await readFile(join(stageDir, "clerk-cli/SKILL.md"), "utf8");
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1];
expect(frontmatter, "SKILL.md must have YAML frontmatter").toBeDefined();

Expand Down
26 changes: 13 additions & 13 deletions packages/cli-core/src/commands/skill/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,22 @@ import {
import { isNonEmpty } from "../../lib/helpers/arrays.js";
import { detectPackageManager, type PackageManager } from "../../lib/package-manager.js";

import clerkSkillMd from "../../../../../skills/clerk/SKILL.md" with { type: "text" };
import clerkAuthMd from "../../../../../skills/clerk/references/auth.md" with { type: "text" };
import clerkRecipesMd from "../../../../../skills/clerk/references/recipes.md" with { type: "text" };
import clerkAgentModeMd from "../../../../../skills/clerk/references/agent-mode.md" with { type: "text" };
import clerkSkillMd from "../../../../../skills/clerk-cli/SKILL.md" with { type: "text" };
import clerkAuthMd from "../../../../../skills/clerk-cli/references/auth.md" with { type: "text" };
import clerkRecipesMd from "../../../../../skills/clerk-cli/references/recipes.md" with { type: "text" };
import clerkAgentModeMd from "../../../../../skills/clerk-cli/references/agent-mode.md" with { type: "text" };

/**
* The bundled clerk skill, as `(relativePath, content)` pairs. Text
* imports resolve live from `<repo-root>/skills/clerk/` during
* The bundled clerk-cli skill, as `(relativePath, content)` pairs. Text
* imports resolve live from `<repo-root>/skills/clerk-cli/` during
* `bun run dev` and get embedded into the compiled binary by
* `bun build --compile`, so the content always matches the CLI being run.
*/
const BUNDLED_CLERK_SKILL: ReadonlyArray<readonly [string, string]> = [
["clerk/SKILL.md", clerkSkillMd],
["clerk/references/auth.md", clerkAuthMd],
["clerk/references/recipes.md", clerkRecipesMd],
["clerk/references/agent-mode.md", clerkAgentModeMd],
["clerk-cli/SKILL.md", clerkSkillMd],
["clerk-cli/references/auth.md", clerkAuthMd],
["clerk-cli/references/recipes.md", clerkRecipesMd],
["clerk-cli/references/agent-mode.md", clerkAgentModeMd],
];

/**
Expand Down Expand Up @@ -82,7 +82,7 @@ export async function withStagedClerkSkill<T>(
version: string | undefined,
fn: (stageDir: string) => Promise<T>,
): Promise<T> {
const stageDir = await mkdtemp(join(tmpdir(), "clerk-skill-"));
const stageDir = await mkdtemp(join(tmpdir(), "clerk-cli-skill-"));
try {
for (const [rel, content] of BUNDLED_CLERK_SKILL) {
const dest = join(stageDir, rel);
Expand Down Expand Up @@ -223,7 +223,7 @@ export async function installClerkSkillCore(
interactive: boolean,
): Promise<boolean> {
return withStagedClerkSkill(resolveCliVersion(), (stageDir) =>
runSkillsAdd(runner, cwd, stageDir, [], interactive, true, "clerk skill"),
runSkillsAdd(runner, cwd, stageDir, [], interactive, true, "clerk-cli skill"),
);
}

Expand All @@ -248,6 +248,6 @@ export async function skillInstall(options: SkillInstallOptions): Promise<void>
const ok = await installClerkSkillCore(runner, cwd, interactive);
if (ok) {
log.blank();
log.success("clerk skill installed. AI agents now have Clerk context in this project.");
log.success("clerk-cli skill installed. AI agents now have Clerk context in this project.");
}
}
Loading
Loading