From 896cf46980cb67820ac7d26d66479e776d8e0e9d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 1 Jun 2026 11:30:32 -0400 Subject: [PATCH 1/5] feat: remove bundled clerk-cli skill Install clerk-cli from clerk/skills during init instead of shipping a bundled local skill. --- .changeset/remove-clerk-cli-skill.md | 5 + README.md | 4 - packages/cli-core/src/cli-program.test.ts | 86 +---- packages/cli-core/src/cli-program.ts | 41 +-- packages/cli-core/src/commands/init/README.md | 17 +- .../cli-core/src/commands/init/skills.test.ts | 7 +- packages/cli-core/src/commands/init/skills.ts | 29 +- .../cli-core/src/commands/skill/README.md | 34 -- .../src/commands/skill/install.test.ts | 155 +-------- .../cli-core/src/commands/skill/install.ts | 128 +------- packages/cli-core/src/lib/next-steps.ts | 4 - packages/cli-core/src/lib/skill-detection.ts | 52 --- skills/clerk-cli/SKILL.md | 261 --------------- skills/clerk-cli/references/agent-mode.md | 287 ----------------- skills/clerk-cli/references/auth.md | 155 --------- skills/clerk-cli/references/recipes.md | 299 ------------------ 16 files changed, 40 insertions(+), 1524 deletions(-) create mode 100644 .changeset/remove-clerk-cli-skill.md delete mode 100644 packages/cli-core/src/commands/skill/README.md delete mode 100644 packages/cli-core/src/lib/skill-detection.ts delete mode 100644 skills/clerk-cli/SKILL.md delete mode 100644 skills/clerk-cli/references/agent-mode.md delete mode 100644 skills/clerk-cli/references/auth.md delete mode 100644 skills/clerk-cli/references/recipes.md diff --git a/.changeset/remove-clerk-cli-skill.md b/.changeset/remove-clerk-cli-skill.md new file mode 100644 index 00000000..642f301a --- /dev/null +++ b/.changeset/remove-clerk-cli-skill.md @@ -0,0 +1,5 @@ +--- +"clerk": major +--- + +Remove the bundled `clerk skill install` command and install the `clerk-cli` agent skill from the [`clerk/skills`](https://github.com/clerk/skills) repository during `clerk init`. diff --git a/README.md b/README.md index 2ee89064..e372cef3 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,7 @@ Commands: api [options] [endpoint] [filter] Make authenticated requests to the Clerk API doctor [options] Check your project's Clerk integration health completion [shell] Generate shell autocompletion script - skill Manage the bundled Clerk CLI agent skill update [options] Update the Clerk CLI to the latest version deploy Deploy a Clerk application to production help [command] Display help for command - -Give AI agents better Clerk context: install the Clerk skills - $ clerk skill install ``` diff --git a/packages/cli-core/src/cli-program.test.ts b/packages/cli-core/src/cli-program.test.ts index 540fbed9..b353340b 100644 --- a/packages/cli-core/src/cli-program.test.ts +++ b/packages/cli-core/src/cli-program.test.ts @@ -1,10 +1,6 @@ -import { test, expect, describe, beforeEach, afterEach } from "bun:test"; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { test, expect, describe } from "bun:test"; import { createProgram, formatApiBody } from "./cli-program.ts"; import { ApiError } from "./lib/errors.ts"; -import { STANDARD_AGENT_DIRS, EXTRA_REL_PATHS } from "./lib/skill-detection.ts"; test("registers users as a top-level command", () => { const program = createProgram(); @@ -12,6 +8,12 @@ test("registers users as a top-level command", () => { expect(users).toBeDefined(); }); +test("does not register the removed clerk skill command", () => { + const program = createProgram(); + const skill = program.commands.find((command) => command.name() === "skill"); + expect(skill).toBeUndefined(); +}); + test("registers users create and list as subcommands", () => { const program = createProgram(); const users = program.commands.find((command) => command.name() === "users")!; @@ -344,77 +346,3 @@ describe("formatApiBody", () => { expect(result).toBe("Plan limitation"); }); }); - -describe("help: clerk skill install tip", () => { - const TIP_SUBSTR = "Give AI agents better Clerk context"; - - // Capture help output including `addHelpText("after", ...)`. The custom - // formatter in lib/help.ts rebuilds help from scratch, so the "after" - // listener only fires during outputHelp (which writes via stdout). - function renderHelp(): string { - const program = createProgram(); - let captured = ""; - const origWrite = process.stdout.write.bind(process.stdout); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (process.stdout as unknown as { write: (chunk: any) => boolean }).write = (chunk) => { - captured += typeof chunk === "string" ? chunk : chunk.toString(); - return true; - }; - try { - program.outputHelp(); - } finally { - (process.stdout as unknown as { write: typeof origWrite }).write = origWrite; - } - return captured; - } - - let tmpHome: string; - let tmpCwd: string; - let originalHome: string | undefined; - let originalCwd: string; - - beforeEach(() => { - originalHome = process.env.HOME; - originalCwd = process.cwd(); - tmpHome = mkdtempSync(join(tmpdir(), "clerk-help-home-")); - tmpCwd = mkdtempSync(join(tmpdir(), "clerk-help-cwd-")); - process.env.HOME = tmpHome; - process.chdir(tmpCwd); - }); - - afterEach(() => { - process.chdir(originalCwd); - if (originalHome === undefined) delete process.env.HOME; - else process.env.HOME = originalHome; - rmSync(tmpHome, { recursive: true, force: true }); - rmSync(tmpCwd, { recursive: true, force: true }); - }); - - test("shows the tip when no skill is installed anywhere", () => { - const help = renderHelp(); - expect(help).toContain(TIP_SUBSTR); - expect(help).toContain("clerk skill install"); - }); - - const AGENT_DIR_CASES = STANDARD_AGENT_DIRS.flatMap((dir) => - (["HOME", "cwd"] as const).map((root) => ({ dir, root })), - ); - - test.each(AGENT_DIR_CASES)( - "hides the tip when $dir/skills/clerk-cli/SKILL.md exists under $root", - ({ dir, root }) => { - const base = root === "HOME" ? tmpHome : tmpCwd; - const target = join(base, dir, "skills/clerk-cli"); - mkdirSync(target, { recursive: true }); - writeFileSync(join(target, "SKILL.md"), "ok"); - expect(renderHelp()).not.toContain(TIP_SUBSTR); - }, - ); - - test.each([...EXTRA_REL_PATHS])("hides the tip when %s exists under cwd", (rel) => { - const full = join(tmpCwd, rel); - mkdirSync(dirname(full), { recursive: true }); - writeFileSync(full, "ok"); - expect(renderHelp()).not.toContain(TIP_SUBSTR); - }); -}); diff --git a/packages/cli-core/src/cli-program.ts b/packages/cli-core/src/cli-program.ts index c4996d1e..bef0fcce 100644 --- a/packages/cli-core/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -29,7 +29,6 @@ import { import { completion, SUPPORTED_SHELLS } from "./commands/completion/index.ts"; import { FRAMEWORK_NAMES } from "./lib/framework.ts"; import { PACKAGE_MANAGERS } from "./lib/package-manager.ts"; -import { skillInstall } from "./commands/skill/install.ts"; import { CliError, UserAbortError, @@ -47,7 +46,6 @@ import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts"; import { update } from "./commands/update/index.ts"; import { deploy } from "./commands/deploy/index.ts"; import { deployStatus } from "./commands/deploy/status-command.ts"; -import { isClerkSkillInstalled } from "./lib/skill-detection.ts"; import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts"; import { billingEnable, billingDisable } from "./commands/billing/index.ts"; import { registerExtras } from "@clerk/cli-extras"; @@ -110,14 +108,7 @@ export function createProgram() { "--mode ", "Force interaction mode (human or agent). Defaults to auto-detect based on TTY.", ) - .option("--verbose", "Show detailed output (enables debug messages)") - .addHelpText("after", () => - isClerkSkillInstalled() - ? "" - : ` -Give AI agents better Clerk context: install the Clerk skills - $ clerk skill install`, - ); + .option("--verbose", "Show detailed output (enables debug messages)"); program.hook("preAction", async () => { // Reset log level at the start of each command invocation so a previous @@ -880,36 +871,6 @@ Tutorial — enable completions for your shell: ) .action(completion); - const skill = program - .command("skill") - .description("Manage the bundled Clerk CLI agent skill") - .setExamples([ - { command: "clerk skill install", description: "Install the clerk agent skill" }, - { - command: "clerk skill install -y", - description: "Install non-interactively (auto-detect agents, global scope)", - }, - ]); - - skill - .command("install") - .description("Install the bundled clerk agent skill") - .option("-y, --yes", "Skip prompts and run the `skills` CLI unattended") - .addOption( - createOption("--pm ", "Package manager hint for runner detection").choices( - PACKAGE_MANAGERS, - ), - ) - .setExamples([ - { command: "clerk skill install", description: "Install with an interactive runner picker" }, - { command: "clerk skill install -y", description: "Install unattended" }, - { - command: "clerk skill install --pm bun", - description: "Force bunx as the runner", - }, - ]) - .action(skillInstall); - program .command("update") .description("Update the Clerk CLI to the latest version") diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index bfde892a..45c7998f 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -190,20 +190,9 @@ 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. -Two install commands run, sharing one runner: - -### 1. The bundled `clerk-cli` skill - -The `clerk-cli` skill ships **inside the CLI binary**. Its markdown files at [`/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 ` skills add --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-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 - -A fixed default set is always installed from [`clerk/skills`](https://github.com/clerk/skills), covering the `core/` and `features/` directories: +A fixed default set is installed from [`clerk/skills`](https://github.com/clerk/skills), covering the `cli/`, `core/`, and `features/` directories: +- **CLI**: `clerk-cli` - **Core**: `clerk-setup`, `clerk-custom-ui`, `clerk-backend-api` - **Features**: `clerk-orgs`, `clerk-testing`, `clerk-webhooks` @@ -226,7 +215,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-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. +The skills install is optional and non-fatal. If the `skills` CLI can't be fetched by the runner or exits non-zero, init prints a yellow warning with a manual install command and still exits successfully. 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. diff --git a/packages/cli-core/src/commands/init/skills.test.ts b/packages/cli-core/src/commands/init/skills.test.ts index 9e7155eb..badda335 100644 --- a/packages/cli-core/src/commands/init/skills.test.ts +++ b/packages/cli-core/src/commands/init/skills.test.ts @@ -2,6 +2,7 @@ import { test, expect, describe } from "bun:test"; import { formatSkillsPromptMessage, resolveUpstreamSkills } from "./skills.ts"; const DEFAULTS = [ + "clerk-cli", "clerk-setup", "clerk-custom-ui", "clerk-backend-api", @@ -11,7 +12,7 @@ const DEFAULTS = [ ]; describe("resolveUpstreamSkills", () => { - test("returns the 6 defaults when no framework is detected", () => { + test("returns the 7 defaults when no framework is detected", () => { expect(resolveUpstreamSkills(undefined)).toEqual(DEFAULTS); }); @@ -35,13 +36,13 @@ describe("resolveUpstreamSkills", () => { describe("formatSkillsPromptMessage", () => { test("summarizes without a framework skill", () => { expect(formatSkillsPromptMessage(undefined)).toBe( - "Install agent skills? (clerk core + features)", + "Install agent skills? (clerk-cli + 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)", + "Install agent skills? (clerk-cli + core + features + nextjs-patterns)", ); }); }); diff --git a/packages/cli-core/src/commands/init/skills.ts b/packages/cli-core/src/commands/init/skills.ts index d113da78..f13bce5a 100644 --- a/packages/cli-core/src/commands/init/skills.ts +++ b/packages/cli-core/src/commands/init/skills.ts @@ -1,16 +1,10 @@ /** * Install Clerk agent skills after scaffolding. * - * Two installer calls share one runner detection: - * - * 1. The bundled `clerk` skill (embedded in the binary via text - * imports). Delegated to the `clerk skill install` core helpers in - * `commands/skill/install.ts`. - * - * 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 upstream skills (`clerk-cli`, `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 @@ -22,10 +16,12 @@ import { isHuman } from "../../mode.js"; import { log } from "../../lib/log.js"; import { confirm } from "../../lib/prompts.js"; import type { ProjectContext } from "./frameworks/types.js"; -import { installClerkSkillCore, resolveSkillsRunner, runSkillsAdd } from "../skill/install.js"; +import { resolveSkillsRunner, runSkillsAdd } from "../skill/install.js"; -/** Upstream skills from clerk/skills — installed on every project (excludes the bundled clerk skill). */ +/** Upstream skills from clerk/skills — installed on every project. */ const DEFAULT_UPSTREAM_SKILLS = [ + // cli/ + "clerk-cli", // core/ "clerk-setup", "clerk-custom-ui", @@ -74,7 +70,7 @@ export function getFrameworkSkill(frameworkDep: string | undefined): string | un function formatSkillsSummary(frameworkSkill: string | undefined): string { const framework = frameworkSkill ? ` + ${frameworkSkill.replace(/^clerk-/, "")}` : ""; - return `clerk core + features${framework}`; + return `clerk-cli + core + features${framework}`; } export function formatSkillsPromptMessage(frameworkSkill: string | undefined): string { @@ -104,11 +100,6 @@ export async function installSkills( const runner = await resolveSkillsRunner(packageManager, interactive); if (!runner) return; - // Install the bundled clerk skill from a staged temp dir, then the - // upstream framework patterns. Each call soft-fails independently so a - // 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, @@ -120,7 +111,7 @@ export async function installSkills( formatSkillsSummary(frameworkSkill), ); - if (cliSkillOk && upstreamOk) { + if (upstreamOk) { log.blank(); log.success("Agent skills installed. AI agents now have Clerk context in this project."); } diff --git a/packages/cli-core/src/commands/skill/README.md b/packages/cli-core/src/commands/skill/README.md deleted file mode 100644 index aa98d301..00000000 --- a/packages/cli-core/src/commands/skill/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Skill Command - -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-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 -clerk skill install -clerk skill install -y -clerk skill install --pm bun -``` - -## Options - -| Option | Description | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `-y, --yes` | Skip prompts; auto-select the preferred package runner and pass `-y -g` to the `skills` CLI | -| `--pm ` | Package manager hint for runner detection (`bun`, `pnpm`, `yarn`, `npm`). Defaults to lockfile detection in the current dir | - -## Clerk API endpoints - -None. This command does not make any Clerk API calls; it only spawns the external `skills` CLI against a staged copy of the bundled skill. diff --git a/packages/cli-core/src/commands/skill/install.test.ts b/packages/cli-core/src/commands/skill/install.test.ts index a5c04971..b14c7319 100644 --- a/packages/cli-core/src/commands/skill/install.test.ts +++ b/packages/cli-core/src/commands/skill/install.test.ts @@ -1,9 +1,5 @@ import { test, expect, describe } from "bun:test"; -import { existsSync } from "node:fs"; -import { readFile, stat } from "node:fs/promises"; -import { join } from "node:path"; -import YAML from "yaml"; -import { buildSkillsArgs, renderSkillVersionPlaceholder, withStagedClerkSkill } from "./install.ts"; +import { buildSkillsArgs } from "./install.ts"; describe("buildSkillsArgs", () => { const skills = ["clerk", "clerk-setup", "clerk-nextjs-patterns"]; @@ -40,156 +36,17 @@ describe("buildSkillsArgs", () => { expect(buildSkillsArgs(upstream, skills, false, false)).not.toContain("--agent"); }); - 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"]); + test("empty skillNames omits --skill flags", () => { + const args = buildSkillsArgs("clerk/skills", [], true, false); + expect(args).toEqual(["skills", "add", "clerk/skills"]); expect(args).not.toContain("--skill"); }); - test("copy=true appends --copy flag (required for the staged clerk-cli dir)", () => { - const args = buildSkillsArgs("/tmp/clerk-cli-skill-xyz", [], false, true); + test("copy=true appends --copy flag", () => { + const args = buildSkillsArgs("clerk/skills", [], false, true); expect(args).toContain("--copy"); // --copy should trail -y / -g, not replace them. expect(args).toContain("-y"); expect(args).toContain("-g"); }); }); - -describe("withStagedClerkSkill", () => { - test("stages all bundled files into a fresh temp dir and cleans up after", async () => { - let observed: { dir: string; files: Record } | null = null; - - await withStagedClerkSkill(undefined, async (dir) => { - const files = { - "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-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-cli")); - expect(entry.isDirectory()).toBe(true); - }); - - expect(observed).not.toBeNull(); - const { dir, files } = observed!; - - // Every file has some content (bundled imports are non-empty). - for (const [rel, content] of Object.entries(files)) { - expect(content.length).toBeGreaterThan(0); - expect(content, rel).toMatch(/\S/); - } - - // SKILL.md should at least contain the YAML frontmatter marker. - expect(files["clerk-cli/SKILL.md"]).toContain("---"); - - // Temp dir is removed once the callback returns. - expect(existsSync(dir)).toBe(false); - }); - - test("propagates callback errors after cleaning up", async () => { - let capturedDir: string | null = null; - - await expect( - withStagedClerkSkill(undefined, async (dir) => { - capturedDir = dir; - throw new Error("boom"); - }), - ).rejects.toThrow("boom"); - - expect(capturedDir).not.toBeNull(); - expect(existsSync(capturedDir!)).toBe(false); - }); -}); - -const ALL_BUNDLED_FILES = [ - "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", () => { - const VERSION_CASES: { version: string | undefined; label: string }[] = [ - { version: "4.5.6", label: "explicit 4.5.6" }, - { version: undefined, label: "undefined → latest" }, - ]; - const VERSION_FILE_CASES = VERSION_CASES.flatMap(({ version, label }) => - ALL_BUNDLED_FILES.map((rel) => ({ version, label, rel })), - ); - - test.each(VERSION_FILE_CASES)( - "substitutes {{CLI_VERSION}} ($label) in $rel", - async ({ version, rel }) => { - await withStagedClerkSkill(version, async (stageDir) => { - const content = await readFile(join(stageDir, rel), "utf8"); - expect(content, rel).not.toContain("{{CLI_VERSION}}"); - }); - }, - ); -}); - -describe("bundled SKILL.md frontmatter", () => { - // Regression guard: the upstream `skills` CLI parses SKILL.md frontmatter as - // strict YAML and silently drops skills whose frontmatter fails to parse. - // An unquoted `: ` inside the description (e.g. `raw HTTP: it handles`) - // triggers "Nested mappings are not allowed in compact mappings" and makes - // `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-cli/SKILL.md"), "utf8"); - const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1]; - expect(frontmatter, "SKILL.md must have YAML frontmatter").toBeDefined(); - - const parsed = YAML.parse(frontmatter!); - expect(typeof parsed.name).toBe("string"); - expect(typeof parsed.description).toBe("string"); - expect(parsed.name.length).toBeGreaterThan(0); - expect(parsed.description.length).toBeGreaterThan(0); - }); - }); -}); - -describe("renderSkillVersionPlaceholder", () => { - test("replaces {{CLI_VERSION}} with the provided version", () => { - const result = renderSkillVersionPlaceholder("Pinned: `bunx clerk@{{CLI_VERSION}}`.", "1.2.3"); - expect(result).toBe("Pinned: `bunx clerk@1.2.3`."); - }); - - test("replaces every occurrence, not just the first", () => { - const result = renderSkillVersionPlaceholder( - "v={{CLI_VERSION}} and again v={{CLI_VERSION}}", - "9.9.9", - ); - expect(result).toBe("v=9.9.9 and again v=9.9.9"); - }); - - test("falls back to `latest` when version is undefined", () => { - const result = renderSkillVersionPlaceholder( - "Install: `npx -y clerk@{{CLI_VERSION}}`.", - undefined, - ); - expect(result).toBe("Install: `npx -y clerk@latest`."); - }); - - test("falls back to `latest` when version is the dev sentinel", () => { - const result = renderSkillVersionPlaceholder("v={{CLI_VERSION}}", "0.0.0-dev"); - expect(result).toBe("v=latest"); - }); - - test("returns content unchanged when no placeholder is present", () => { - const input = "No placeholder here."; - expect(renderSkillVersionPlaceholder(input, "1.2.3")).toBe(input); - }); -}); diff --git a/packages/cli-core/src/commands/skill/install.ts b/packages/cli-core/src/commands/skill/install.ts index 223f879c..56e107a7 100644 --- a/packages/cli-core/src/commands/skill/install.ts +++ b/packages/cli-core/src/commands/skill/install.ts @@ -1,30 +1,14 @@ /** - * `clerk skill install` — installs the bundled `clerk` skill for local - * agents. - * - * The `clerk` skill is embedded in the CLI binary via text imports at - * compile time. At install time we stage it to a temp dir and invoke - * `skills add --copy`, so the installed files are full copies - * (not symlinks into the temp dir, which would break when cleaned up). + * Helpers for invoking the external `skills` CLI. * * The external `skills` CLI handles agent auto-detection and scope * selection: in interactive mode we hand off entirely (no `--agent` / `-y`), * so the user gets the native picker. In non-interactive mode we pass * `-y -g` so it runs unattended with global scope and auto-detected agents. - * - * The `init` command also imports `installClerkSkillCore` and - * `resolveSkillsRunner` from here so a single runner detection is shared - * with the upstream framework-pattern skills install. */ -import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; - import { dim } from "../../lib/color.js"; -import { isHuman } from "../../mode.js"; import { log } from "../../lib/log.js"; -import { DEV_CLI_VERSION, resolveCliVersion } from "../../lib/version.js"; import { select } from "../../lib/listage.js"; import { type Runner, @@ -34,67 +18,7 @@ import { runnerForPackageManager, } from "../../lib/runners.js"; import { isNonEmpty } from "../../lib/helpers/arrays.js"; -import { detectPackageManager, type PackageManager } from "../../lib/package-manager.js"; -import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.js"; - -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-cli skill, as `(relativePath, content)` pairs. Text - * imports resolve live from `/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 = [ - ["clerk-cli/SKILL.md", clerkSkillMd], - ["clerk-cli/references/auth.md", clerkAuthMd], - ["clerk-cli/references/recipes.md", clerkRecipesMd], - ["clerk-cli/references/agent-mode.md", clerkAgentModeMd], -]; - -/** - * Substitute `{{CLI_VERSION}}` in a skill asset with the provided version. - * When the version is absent (undefined) or is the dev-build sentinel, the - * placeholder resolves to `latest` so the runnable commands in the skill - * remain copy-pasteable on unversioned binaries. - * - * Exported for tests. - */ -export function renderSkillVersionPlaceholder( - content: string, - version: string | undefined, -): string { - const resolved = version && version !== DEV_CLI_VERSION ? version : "latest"; - return content.replaceAll("{{CLI_VERSION}}", resolved); -} - -/** - * Write the bundled clerk skill to a fresh temp dir and call `fn` with - * its path. Every asset has `{{CLI_VERSION}}` rendered against `version` first - * (see {@link renderSkillVersionPlaceholder}). The dir is deleted on return, - * so `fn` must finish any work that reads from it before returning. - * - * Exported for tests. - */ -export async function withStagedClerkSkill( - version: string | undefined, - fn: (stageDir: string) => Promise, -): Promise { - const stageDir = await mkdtemp(join(tmpdir(), "clerk-cli-skill-")); - try { - for (const [rel, content] of BUNDLED_CLERK_SKILL) { - const dest = join(stageDir, rel); - await mkdir(dirname(dest), { recursive: true }); - await writeFile(dest, renderSkillVersionPlaceholder(content, version)); - } - return await fn(stageDir); - } finally { - await rm(stageDir, { recursive: true, force: true }); - } -} +import type { PackageManager } from "../../lib/package-manager.js"; /** * Build the runner-agnostic argv for `skills add ...`. The caller @@ -102,11 +26,10 @@ export async function withStagedClerkSkill( * {@link runnerCommand}. * * `skillNames` becomes `--skill ` pairs; leave empty to install every - * skill from `source` (what we do for the bundled clerk source). + * skill from `source`. * * `copy` forces the `skills` CLI to copy files into each agent dir instead - * of symlinking. Required for sources that live in an ephemeral directory - * (our staged clerk skill); optional otherwise. + * of symlinking. * * Interactive mode: hand off to the skills CLI's native UX (auto-detect * installed agents, scope picker) by omitting `--agent` and `-y`. @@ -210,46 +133,3 @@ export async function resolveSkillsRunner( return preferred; } - -/** - * Install the bundled clerk skill using a pre-resolved runner. Does not - * prompt; callers handle any UX around confirmation and runner selection. - * - * Shared with the init flow so runner detection happens once when installing - * clerk alongside the upstream framework-pattern skills. - */ -export async function installClerkSkillCore( - runner: Runner, - cwd: string, - interactive: boolean, -): Promise { - return withStagedClerkSkill(resolveCliVersion(), (stageDir) => - runSkillsAdd(runner, cwd, stageDir, [], interactive, true, "clerk-cli skill"), - ); -} - -export interface SkillInstallOptions { - yes?: boolean; - pm?: PackageManager; -} - -/** - * `clerk skill install` — standalone install of the bundled clerk skill. - */ -export async function skillInstall(options: SkillInstallOptions): Promise { - const cwd = process.cwd(); - const skipPrompt = options.yes ?? false; - const interactive = isHuman() && !skipPrompt; - - const packageManager = options.pm ?? (await detectPackageManager(cwd)); - - const runner = await resolveSkillsRunner(packageManager, interactive); - if (!runner) return; - - const ok = await installClerkSkillCore(runner, cwd, interactive); - if (!ok) return; - - log.blank(); - log.success("clerk-cli skill installed. AI agents now have Clerk context in this project."); - printNextSteps(NEXT_STEPS.SKILL_INSTALL); -} diff --git a/packages/cli-core/src/lib/next-steps.ts b/packages/cli-core/src/lib/next-steps.ts index 692c6c3e..807e507b 100644 --- a/packages/cli-core/src/lib/next-steps.ts +++ b/packages/cli-core/src/lib/next-steps.ts @@ -53,10 +53,6 @@ export const NEXT_STEPS = { "Run `clerk link` to connect this directory to a different application", "Run `clerk apps list` to browse your applications", ], - SKILL_INSTALL: [ - "Start a new Claude Code or Codex session — the skill is now active for this project", - "Or run `clerk init` to scaffold Clerk yourself", - ], CONFIG_PUSH: [ "Run `clerk config pull` to confirm the live configuration", "Run `clerk doctor` to verify your setup", diff --git a/packages/cli-core/src/lib/skill-detection.ts b/packages/cli-core/src/lib/skill-detection.ts deleted file mode 100644 index 3bf44ee3..00000000 --- a/packages/cli-core/src/lib/skill-detection.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { existsSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -/** - * Agents that host skills under the uniform - * `/skills//SKILL.md` layout. Each is checked under - * both `$HOME//skills/clerk-cli/SKILL.md` (global) and - * `//skills/clerk-cli/SKILL.md` (project-local). - */ -export const STANDARD_AGENT_DIRS = [ - ".claude", // Claude Code - ".agents", // generic / shared - ".codex", // OpenAI Codex CLI - ".cursor", // Cursor - ".windsurf", // Windsurf / Codeium - ".zed", // Zed editor - ".cline", // Cline VS Code extension -] as const; - -const STANDARD_SKILL_REL = "skills/clerk-cli/SKILL.md"; - -/** - * Paths for agents that don't follow the uniform - * `/skills//SKILL.md` layout. These are project-local only - * and are checked under `` (not `$HOME`), because - * `clerk skill install` does not install these layouts globally. - */ -export const EXTRA_REL_PATHS = [ - ".vscode/skills/clerk-cli/SKILL.md", // VS Code (project-local) - ".github/prompts/clerk-cli.md", // GitHub Copilot (project-local) -] as const; - -/** - * Best-effort synchronous check for whether the bundled `clerk-cli` skill - * is installed for at least one local agent. Returns `true` on the first - * hit. A missed match just means the install tip keeps showing — no - * false positives harm the user. - */ -export function isClerkSkillInstalled(): boolean { - const home = process.env.HOME ?? homedir(); - const cwd = process.cwd(); - - for (const dir of STANDARD_AGENT_DIRS) { - if (existsSync(join(home, dir, STANDARD_SKILL_REL))) return true; - if (existsSync(join(cwd, dir, STANDARD_SKILL_REL))) return true; - } - for (const rel of EXTRA_REL_PATHS) { - if (existsSync(join(cwd, rel))) return true; - } - return false; -} diff --git a/skills/clerk-cli/SKILL.md b/skills/clerk-cli/SKILL.md deleted file mode 100644 index 6e04bf66..00000000 --- a/skills/clerk-cli/SKILL.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -name: clerk-cli -description: >- - Operate the Clerk CLI (`clerk` binary) for authentication, user/org/session management, - deploy verification, instance config, env keys, and any Clerk Backend or Platform API - call. Use when the user mentions Clerk management tasks, "list clerk users", "create a - clerk user", "update organization", "pull clerk config", "clerk env pull", - "clerk doctor", "clerk deploy", "clerk deploy status", "clerk api", or any ad-hoc - Clerk API request. Prefer the CLI over raw HTTP: it handles auth, key resolution, - app/instance targeting, and formatting automatically. ---- - -# Clerk CLI - -The `clerk` binary is a pre-authenticated gateway to Clerk's Backend API and Platform API, plus project-level tooling (auth, linking, env pulls, instance config). When the user asks anything that touches a Clerk resource, reach for `clerk` first instead of hand-rolling `curl`. - -> This skill was installed by `clerk init` (or `clerk skill install`) and is pinned to clerk `{{CLI_VERSION}}`. If `clerk --version` disagrees, refresh it with `clerk skill install` (or `bunx clerk@{{CLI_VERSION}} skill install`). The binary is always the source of truth, so run `clerk --help` to verify anything this skill claims. - -## Execution environment (prefer the host, understand the sandbox warning) - -Most AI coding agents default to running shell commands in a sandbox where the -user's home directory, OS keychain, browser launch, localhost callback -binding, or network access may be blocked. The Clerk CLI depends on all of -those host capabilities: - -- **OS credential store**: `clerk auth login` stores the OAuth token in the - system keychain. A sandbox without keychain access reports "not logged in" - even when the host is authenticated. -- **Home-directory Clerk state**: saved config, cached metadata, and fallback - credentials live under the user's Clerk config/data directories. -- **Linked project metadata**: resolved from the repo's git remote plus Clerk - config. Sandboxes with stripped repo state or blocked home-dir reads can - misreport "not linked". -- **Local `.env*` files**: publishable and secret keys materialized by - `clerk env pull`. -- **Outbound network access to Clerk**: every Backend and Platform API call. -- **Browser + localhost OAuth callback**: `clerk auth login` needs both. - -In agent mode, the CLI now does a **best-effort warn-once check** at the -host-sensitive library boundaries. When it detects that host-only Clerk state -or system capabilities are unavailable, it emits: - -```text -Host-only Clerk state or system capabilities may be unavailable in agent mode. This may be a sandboxed run. -Re-run this command on the host shell before trusting auth, link, env, or API failures. -``` - -Treat that warning as authoritative. The command may continue, but any auth, -link, env, config, API, browser, or OAuth callback failure from that -invocation is untrusted until you rerun the same command on the host. - -**Prefer these commands on the user's host shell, not in a sandbox:** - -`clerk doctor`, `clerk whoami`, `clerk auth login`, `clerk link`, `clerk env pull`, -`clerk apps ...`, `clerk config ...`, `clerk api ...`. - -If a command was accidentally run in a sandbox and it reports `Not logged in`, -`auth_required`, `not linked`, missing env, keychain/file permission errors, -or network failures, **do not treat the result as authoritative**. Rerun it on -the host before acting on it or reporting it to the user. - -## Invoking the CLI - -Before running any `clerk` command, figure out which binary to invoke and bind that choice for the rest of the session: - -```sh -# 1. Prefer a globally installed binary when it matches the skill's pinned version. -command -v clerk >/dev/null 2>&1 && clerk --version -``` - -If that prints `{{CLI_VERSION}}` (or any version you trust), use bare `clerk` for the rest of the session. - -Otherwise fall back to a package runner, in this order (matches the CLI's own `preferredRunner` logic, which prefers the runner that matches the project's lockfile): - -| Project package manager | Invocation | -| ------------------------- | -------------------------------- | -| bun (`bun.lock*`) | `bunx clerk@{{CLI_VERSION}}` | -| npm (`package-lock.json`) | `npx -y clerk@{{CLI_VERSION}}` | -| pnpm (`pnpm-lock.yaml`) | `pnpm dlx clerk@{{CLI_VERSION}}` | -| yarn >= 2 (`yarn.lock`) | `yarn dlx clerk@{{CLI_VERSION}}` | - -Yarn Classic (v1) has no `dlx`; treat those projects as "no preferred runner" and fall back to the first runner from the list above that's on PATH. - -The published npm package is **`clerk`**, not `@clerk/cli`. Never teach `npm install -g clerk` as the primary path. The bundled skill is versioned alongside the binary, so a globally installed mismatched version will drift. If `clerk --version` disagrees with `{{CLI_VERSION}}`, either upgrade the global install or fall back to the pinned-runner form above. - -## Prerequisites (run at session start) - -Before running any other Clerk command in a session, verify the CLI is authenticated, linked, and healthy: - -```sh -clerk --version # confirm the binary is on PATH -clerk doctor --json # structured health check; exit 1 if anything failed -``` - -**Always run `clerk doctor --json` first.** It catches the common setup failures (not logged in, project not linked, missing keys, stale CLI version) up front, so later commands don't fail with confusing errors. In agent mode it also includes a `Host execution` check that warns when Clerk's host-side config / credential directories are not writable, which is the canonical signal that the current invocation is likely sandboxed. - -Each result has `name`, `status` (`pass`/`warn`/`fail`), `message`, optional `detail`, optional `remedy` (how to fix it), and optional `fix` (label for auto-fixable issues). Parse that and act on it, or surface it to the user. If `Host execution` warns, rerun the command on the host before trusting any auth/link/env/API failures from the same sandboxed run. Rerun `clerk doctor --json` whenever a later command starts misbehaving. - -If `clerk --version` reports a newer CLI than this skill (which is pinned to `{{CLI_VERSION}}`), run `clerk skill install` to refresh the bundled skill. The CLI binary is always the source of truth. - -## The mental model - -| Layer | What it does | Commands | -| ------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| **Session / project** | Auth, link a repo to a Clerk app, pull env keys | `auth login`, `link`, `unlink`, `whoami`, `env pull`, `doctor` | -| **Instance config** | Manage the configuration (social providers, session lifetimes, etc.) for a specific instance | `config pull`, `config schema`, `config patch`, `config put` | -| **Backend API (default)** | Runtime data: users, orgs, sessions, invitations, JWT templates, webhooks | `clerk api ` | -| **Platform API (`--platform`)** | Account-level: applications, instances, billing | `clerk api --platform ` | - -A project is "linked" to an application via `clerk link`. Once linked, most commands auto-resolve the target app and dev instance from the repo's git remote. To target something else, pass `--app ` and/or `--instance dev|prod|`. See [references/auth.md](references/auth.md) for the full resolution order. - -## Discover endpoints — don't memorize them - -The CLI ships with the Clerk OpenAPI catalog. Always discover endpoints dynamically instead of guessing paths: - -```sh -clerk api ls # list every Backend API endpoint -clerk api ls users # filter by keyword (matches path, summary, tag, operationId) -clerk api ls --platform apps # list Platform API endpoints -``` - -Use this before `clerk api `. If you don't see the endpoint you expected, it probably isn't exposed. - -## The `clerk api` command (the workhorse) - -`clerk api` makes authenticated HTTP calls. It auto-resolves keys, auto-detects method from body presence, supports stdin, and can preview mutations with `--dry-run`. - -```sh -# GET requests -clerk api /users # list users -clerk api /users/user_abc123 # fetch one -clerk api /users?limit=5&order_by=-created_at # query params work inline - -# Mutating requests -clerk api /users -d '{"email_address":["a@b.co"]}' # POST (auto-detected from body) -clerk api /users/user_abc123 -X PATCH -d '{"first_name":"A"}' -clerk api /users/user_abc123 -X DELETE - -# Body from file or stdin -clerk api /users --file payload.json -cat payload.json | clerk api /users - -# Always preview mutations first -clerk api /users/user_abc123 -X DELETE --dry-run -clerk api /users/user_abc123 -X DELETE --yes # skip confirmation once you've verified - -# Target a specific app/instance -clerk api /users --app app_abc123 --instance prod - -# Include response headers when debugging -clerk api /users --include - -# Platform API (account-level, not tenant data) -clerk api /v1/platform/applications --platform -``` - -For instance config, prefer the dedicated `clerk config ...` commands over raw Platform API `/config` paths. They handle dry-run, diffing, and confirmation more cleanly than the raw endpoint form. - -**Always `--dry-run` a mutation before running it for real.** Then re-run without `--dry-run` (add `--yes` if you're sure). In agent mode, interactive confirmation is bypassed, so `--dry-run` is the only safety net for destructive calls. - -**JSON bodies must be valid JSON.** The CLI validates and rejects malformed payloads. - -**Endpoint paths may be given with or without `/v1/` prefix** — both work for Backend API calls. The CLI normalizes. - -See [references/recipes.md](references/recipes.md) for concrete patterns: listing/filtering users, creating orgs, impersonation sessions, etc. - -## Inspecting large outputs (do not flood your context) - -`users list`, `apps list`, `config pull`, and most `clerk api` GETs return payloads that can be many kilobytes or megabytes. Production tenants commonly have thousands of users; an instance config can be hundreds of fields deep. Reading those responses into the conversation costs context window for no benefit. Save the response to a file first, then query just what you need with `jq`: - -```sh -# 1. Persist the response. Use --limit 250 to maximize page size for users list. -clerk users list --json --limit 250 > /tmp/users.json -clerk apps list --json > /tmp/apps.json -clerk api /users/user_abc123 > /tmp/user.json - -# 2. Inspect only what you need. -jq '.data | length' /tmp/users.json # current page size -jq '.hasMore' /tmp/users.json # are more pages available? -jq '.data[0] | keys' /tmp/users.json # discover the user shape once -jq '.data[] | {id, email_addresses}' /tmp/users.json # project to a few fields -jq '[.data[] | select(.banned)] | length' /tmp/users.json # aggregate without reading rows -``` - -**If `jq` is not available**, fall back to Python or Node — both can stream the file without printing it whole: - -```sh -python3 -c 'import json; d=json.load(open("/tmp/users.json")); print(len(d["data"]), d["hasMore"])' -node -e 'const d=require("/tmp/users.json"); console.log(d.data.length, d.hasMore)' -``` - -`cat` / `head` the file only when you genuinely need to see the raw structure for one-off debugging. When walking pages, write each page to its own file (e.g. `page-${offset}.json`) so individual pages stay independently inspectable. - -## Core commands at a glance - -| Command | Purpose | Key flags | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `clerk init` | Scaffold Clerk into a project. `--starter` only supports bootstrap for Next.js, React Router, Astro, Nuxt, TanStack Start, React, Vue, and JavaScript. | `--framework`, `--pm`, `--name` (with `--starter`), `--app`, `--starter`, `-y`, `--no-skills` | -| `clerk auth login` | OAuth browser login (stores token). Agent mode: no-op if already logged in. With no stored session it still opens a browser and binds a localhost callback, so it is not unattended; prefer `CLERK_PLATFORM_API_KEY` for headless flows. Aliases: `signup`, `signin`, `sign-in`. Top-level shortcut: `clerk login`. | — | -| `clerk auth logout` | Clear stored credentials. Aliases: `signout`, `sign-out`. Top-level shortcut: `clerk logout`. | — | -| `clerk whoami` | Print the logged-in email. | — | -| `clerk link` / `clerk unlink` | Link this repo to a Clerk app, or remove the link. `unlink` requires `--yes` in agent mode. | (see `--help`) | -| `clerk env pull` | Write publishable + secret keys to the framework's env file (merge, not clobber). Resolves `.env.development.local` → framework-preferred file → `.env.local`; override with `--file`. | (see `--help`) | -| `clerk config {pull,schema}` | Fetch instance config JSON, or its JSON Schema. | (see `--help`) | -| `clerk config patch` | Partial update (PATCH) of instance config. Pass `--destructive` to actually delete sub-resources touched by the patch rather than resetting them to defaults. | `--app`, `--instance`, `--file`, `--json`, `--dry-run`, `--yes`, `--destructive` | -| `clerk config put` | Full replacement (PUT) of instance config. Pass `--destructive` to actually delete removed sub-resources rather than resetting them to defaults. | `--app`, `--instance`, `--file`, `--json`, `--dry-run`, `--yes`, `--destructive` | -| `clerk apps {list,create}` | List or create Clerk applications. Defaults to JSON in agent mode. | (see `--help`) | -| `clerk users` (no subcommand) | Interactive picker for `users` actions in human mode; in agent mode prints the action list and exits `2`. Always pass an explicit subcommand from agents. | `--app`, `--instance`, `--secret-key` | -| `clerk users list` | List users via curated BAPI flags. JSON output (default when piped or in agent mode) is `{data, hasMore}` so callers can paginate without `/users/count`. `--limit` defaults to 100 (max 250). | `--limit`, `--offset`, `--query`, `--email-address`, `--phone-number`, `--username`, `--user-id`, `--external-id`, `--order-by`, `--json`, `--app`, `--instance`, `--secret-key` | -| `clerk users create` | Create a user from curated flags or a raw BAPI body. Confirmation prompt unless `--yes`. | `--email`, `--phone`, `--username`, `--password`, `--first-name`, `--last-name`, `--external-id`, `-d, --data`, `--file`, `--dry-run`, `--yes`, `--json` | -| `clerk users open [user-id]` | Open a user's dashboard page. Agent mode requires `user-id` and prints a JSON descriptor instead of launching a browser. | (see `--help`) | -| `clerk open [subpath]` | Open the linked app's dashboard in a browser. Agent mode: prints a JSON descriptor instead of opening. | (see `--help`) | -| `clerk deploy` | Human-mode production deploy wizard. Agent mode: emits a read-only JSON handoff and tells the agent whether to ask the human to run the wizard, wait for provisioning, finish OAuth, or do nothing. | `--mode agent`, `--mode human`, `--verbose` | -| `clerk deploy status` | Read-only deploy verification. Triggers a DNS check, reports aggregate domain and OAuth readiness, and exits `0` only when complete. Agent mode does one quick check by default; pass `--wait` to keep waiting. | `--mode agent`, `--wait`, `--verbose` | -| `clerk doctor` | Health check (CLI version, login, link, env, config, completion; plus host-execution probe in agent mode). | `--json`, `--spotlight`, `--verbose`, `--fix` | -| `clerk api [path]` | Authenticated HTTP to Backend/Platform API. | `-X`, `-d`, `--file`, `--dry-run`, `--yes`, `--include`, `--app`, `--secret-key`, `--instance`, `--platform` | -| `clerk api ls [filter]` | Discover endpoints from the bundled OpenAPI catalog. | (see `--help`) | -| `clerk completion [shell]` | Print a shell completion script (`bash`, `zsh`, `fish`, `powershell`). | — | -| `clerk update` | Update the CLI to the latest version. | `--channel`, `-y`, `--all` | -| `clerk skill install` | Reinstall the bundled `clerk-cli` skill. Run after upgrading the CLI so the skill matches the new binary. | (see `--help`) | - -**`clerk --help` is the source of truth for flags.** This table is a hint, not a spec. Before running an unfamiliar command or flag combination, run `clerk --help` once per session. Every command also defines `setExamples([...])` in source, which `--help` renders as a copy-pasteable Examples block, so you rarely need to guess syntax. - -## Agent-mode behavior (important) - -The CLI auto-detects agent mode when stdout is not a TTY, or when `--mode agent` / `CLERK_MODE=agent` is set. In agent mode: - -- **Interactive prompts are disabled.** Commands that would normally show pickers (`link` without `--app`, `unlink` without `--yes`, `users` without a subcommand) either auto-resolve or exit with a usage error. `clerk api` with no args prints usage guidance and exits 0; pass an endpoint (or `ls`) explicitly. Always pass explicit flags (`--app`, `--yes`) in scripted calls. -- **Host-sensitive operations emit a sandbox warning once per invocation.** Home-directory Clerk state, keychain access, networked Clerk calls, browser launch, and localhost OAuth callback setup can trigger the warning shown above. If it appears, rerun the same command on the host before trusting the result. -- **If your harness does not clearly present as agent mode, force it.** Use `--mode agent` or `CLERK_MODE=agent` when you want the CLI's non-interactive behavior and sandbox warning path to apply deterministically. -- **`link` supports deterministic agent flows.** In agent mode, `clerk link --app ` links directly. Without `--app`, the CLI will try silent key-based autolink first; if it cannot determine the app unambiguously, it exits and tells you to pass `--app`. -- **`init` never selects or creates a real Clerk app for you in agent mode unless authenticated or given a target.** Pass `--app ` (or pre-link the project) to authenticate and link a real app, or pass `--keyless` to use auto-generated temporary development keys when bootstrapping a new project on a keyless-capable framework. Without either, agent mode prints manual setup guidance and exits cleanly. -- **`unlink` requires `--yes` in agent mode.** This preserves the same safety bar as other destructive commands while still letting an agent complete the unlink non-interactively. -- **Mutations still require `--yes`** unless you accept per-call confirmation is impossible. -- **`doctor --fix` is ignored.** Parse `doctor --json` output's `remedy` field and act on it yourself. -- **`apps list` and `apps create` default to JSON** when piped. -- **`users` defaults to JSON when piped, like `apps`.** `clerk users list` and `clerk users create` emit JSON in agent mode. Bare `clerk users` (no subcommand) is a usage error in agent mode — pass `list`, `create`, or `open` explicitly. `clerk users open` requires the `user-id` positional in agent mode and prints a JSON descriptor instead of launching a browser. -- **`deploy` has an agent handoff plus a verification gate.** In agent mode, bare `clerk deploy` is read-only and emits a JSON handoff. It never drives the interactive wizard. Do not tell Claude or another agent to run `! clerk deploy`, because the wizard needs interactive stdin prompts. Ask the human to run `clerk deploy` in a new terminal window when needed, then run `clerk deploy status --mode agent` to verify completion. See [references/agent-mode.md](references/agent-mode.md#deploy-handoff-and-verification). -- **`--input-json `** expands JSON into flags on any command (e.g. `clerk init --input-json '{"framework":"next","yes":true}'`). Piped stdin is also accepted: `echo '{"yes":true}' | clerk init`. Place `--input-json` after the leaf subcommand. Full rules in [references/agent-mode.md](references/agent-mode.md#passing-options-as-json---input-json). - -Full matrix and sandbox details in [references/agent-mode.md](references/agent-mode.md). - -## Output format and errors - -- **JSON output:** `--json` on `apps list` and `doctor`. For `clerk api`, the response body is the raw API JSON, so pipe into `jq` freely. -- **Exit codes:** `0` success, `1` runtime error, `2` usage/validation error. `doctor` returns `1` if any check failed. -- **Error format:** User-facing errors print a single line to stderr and set a non-zero exit code. Use `--verbose` for stack traces when debugging. - -## Safety rules for autonomous use - -1. **Discover before acting:** `clerk api ls ` before `clerk api `. -2. **Preview mutations:** `--dry-run` on every `config patch`, `config put`, `api -X POST/PATCH/PUT/DELETE`. -3. **Target explicitly in production:** pass `--instance prod` rather than relying on defaults, and confirm with the user before any production mutation. -4. **Never commit secrets:** `env pull` writes to `.env.local` (which should be gitignored). Don't paste secret keys into code or chat. -5. **Use `doctor --json`** to diagnose before assuming the CLI is broken. - -## References - -- [references/auth.md](references/auth.md) — auth flow, key resolution order, host-vs-sandbox behavior, `--app`/`--instance` targeting, Backend vs Platform API. -- [references/recipes.md](references/recipes.md) — copy-pasteable recipes for common Clerk tasks. -- [references/agent-mode.md](references/agent-mode.md) — agent-mode behavior matrix, sandbox warning semantics, exit codes, error format. diff --git a/skills/clerk-cli/references/agent-mode.md b/skills/clerk-cli/references/agent-mode.md deleted file mode 100644 index 3b1e4cf2..00000000 --- a/skills/clerk-cli/references/agent-mode.md +++ /dev/null @@ -1,287 +0,0 @@ -# Clerk CLI — Agent Mode Reference - -The Clerk CLI has a first-class "agent" mode that's designed for non-interactive and AI-driven use. Read this before writing scripts or letting an LLM drive the CLI. - -## Sandbox warning semantics - -Agent mode and sandboxing are related but not identical: - -- **Agent mode** controls non-interactive behavior. -- **Sandboxing** controls whether the CLI can actually reach host-only Clerk - state and host system capabilities. - -In agent mode, the CLI now performs a **best-effort warn-once check** at the -host-sensitive integration boundaries. The first time an invocation hits a -blocked host capability, it emits: - -```text -Host-only Clerk state or system capabilities may be unavailable in agent mode. This may be a sandboxed run. -Re-run this command on the host shell before trusting auth, link, env, or API failures. -``` - -Treat that warning as authoritative. The command may still continue and return -an ordinary Clerk error, but any auth/link/env/config/API/browser/OAuth -failure from that invocation should be treated as suspect until rerun on the -host. - -The warning can be triggered by: - -- home-directory Clerk config / credential file access -- OS keychain access -- outbound Clerk network requests -- browser launch -- localhost callback server binding for OAuth - -If your harness does not obviously look non-interactive, force agent behavior -with `--mode agent` or `CLERK_MODE=agent` so the CLI's non-interactive and -sandbox-warning paths apply deterministically. - -## How agent mode is detected - -Priority (first match wins): - -1. `--mode agent` flag on the command line -2. `CLERK_MODE=agent` environment variable -3. Stdout is not a TTY (piped, redirected, or running under an agent harness) - -Force human mode with `--mode human` or `CLERK_MODE=human`. Typical AI-agent invocations automatically land in agent mode because stdout is piped. - -## What changes in agent mode - -| Behavior | Human mode | Agent mode | -| ---------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Interactive pickers (`link` without `--app`, `api` with no args) | Show a TUI picker | Print structured guidance and exit, or auto-resolve | -| `clerk link --app ` | Links directly | Links directly | -| `clerk link` without `--app` | Interactive picker / create UI | Tries silent autolink from detected publishable keys; if no deterministic match exists, exits with a usage error telling the caller to pass `--app` | -| Confirmation prompts (`unlink`, `config patch`, `api -X DELETE`) | Prompt y/n | Require `--yes`, otherwise error | -| `clerk doctor --fix` | Interactively offers fixes | **Ignored**; output the `remedy` field and let the caller act | -| `clerk apps list` default output | Table | JSON (when piped) | -| `clerk apps create ` output | Human-readable summary | JSON (auto-detected, same as `apps list`); `--json` also works explicitly | -| `clerk users list` / `clerk users create` default output | Table / human-readable | JSON (auto-detected when piped); `--json` also works explicitly | -| `clerk users` (no subcommand) | Interactive action picker | Prints the action list and exits with a usage error (code `2`) — pass `list` / `create` / `open` | -| `clerk users open [user-id]` | Picks a user interactively, opens browser | Requires `user-id`; prints `{url, appId, appName, instanceId, instanceLabel, userId, opened: false}` and does not open a browser | -| `clerk open [subpath]` | Opens the browser to the URL | Does not open a browser. Prints a JSON descriptor (`{url, appId, appName, instanceId, instanceLabel, subpath, opened: false}`) on stdout so the agent can surface it | -| `clerk deploy` | Interactive production deploy wizard | Read-only handoff. Emits deploy status JSON on stdout and exits `0` for linked projects. Does not prompt, mutate, trigger DNS checks, or poll. | -| `clerk deploy status` | Verify production deploy state | Read-only verification gate. Triggers one DNS check for active production domains, waits briefly, reads one live status snapshot, emits status JSON on stdout, exits `0` when complete and `1` when incomplete. It does not keep waiting or back off in agent mode unless `--wait` is passed. Use `--wait` when the user asks the agent to keep waiting for DNS, SSL, email DNS, or final Clerk-side readiness. | -| `clerk auth login` when already authenticated | Prompt to re-auth | Silent no-op | -| `clerk init` | Full interactive scaffold flow | Runs non-interactively. Explicit `--app` or a linked profile uses the real-app auth/link/env flow; without it, an authenticated agent on a keyless-capable framework creates a real app and links it. Pass `--keyless` to opt into auto-generated dev keys for new projects on keyless-capable frameworks. Without `--keyless`, an unauthenticated agent (or non-keyless framework with no app target) prints manual setup guidance. | -| Color / spinners | Enabled | Disabled | - -In addition, sandboxed agent-mode invocations may emit the warning above once -per CLI invocation when a host-sensitive operation is blocked. - -**Rule of thumb:** always pass `--yes` for mutations and `--json` for structured output where available. Pass `--app` / `--instance` when you intentionally target a real app; pass `--keyless` to opt into auto-generated dev keys when bootstrapping a new project without authenticating. - -## Passing options as JSON: `--input-json` - -Every command accepts `--input-json `. Keys convert from camelCase/snake_case to kebab-case and expand into flags before Commander parses argv — so anything a command accepts as a flag can come from JSON instead. - -```sh -clerk init --input-json '{"framework":"next","yes":true}' -clerk config pull --input-json '{"keys":["auth_email","session"]}' # arrays → repeated flags -clerk init --input-json @init-opts.json # read JSON from a file -clerk init --input-json - # read JSON from stdin -echo '{"framework":"next","yes":true}' | clerk init # auto-detect piped stdin -``` - -When `--input-json` is omitted and stdin is piped (not a TTY), the CLI automatically reads JSON from stdin — no flag needed. This lets agents pipe options directly: `echo '{"yes":true}' | clerk init`. - -Positional arguments (e.g. the `` in `clerk apps create `) cannot come from JSON — only flag-style options can. - -| JSON | Expansion | -| ---------------- | --------------------------------------- | -| `"str"` / number | `--flag ` | -| `true` | `--flag` | -| `false` / `null` | omitted | -| `["a","b"]` | `--flag a --flag b` (empty arrays omit) | -| `{…}` (nested) | rejected — `invalid_json`, exit `2` | - -**Placement.** Put `--input-json` after the leaf subcommand. Before it, flags land on the root program, so only `--mode` / `--verbose` work there — subcommand flags (`--json`, `--app`, etc.) error as unknown. Explicit flags after `--input-json` override its values (last-flag-wins). - -Errors use the standard agent-mode format: bad JSON → `invalid_json`, missing `@file` → `file_not_found`, unknown expanded flags → Commander's `unknown option`. All exit `2`. - -## Exit codes - -| Code | Meaning | -| ---- | ---------------------------------------------------------------------------- | -| `0` | Success | -| `1` | Runtime error (auth failure, API error, file I/O, etc.) | -| `2` | Usage or validation error (bad flags, malformed JSON body, unknown endpoint) | - -`clerk doctor` exits `1` when any check fails (warnings alone still exit `0`). - -## Error output format - -**Human mode:** - -- Single-line error message on stderr. -- Stack traces hidden unless `--verbose` is passed. -- API errors include the first message from the response body, prefixed with a human context string (e.g., `Failed to fetch config: unauthorized`). - -**Agent mode:** - -- Structured JSON on stderr: `{"error":{"code":"...","message":"...","docsUrl?":"...","errors?":[...]}}`. -- `code` is a machine-readable error code (e.g., `auth_required`, `api_error`, `unexpected_error`). -- `errors` array is present for API errors and mirrors the Clerk API error shape (`{code?, message?, meta?}`). -- `docsUrl` is present when the error has associated documentation. - -**Both modes:** - -- User-aborted commands exit cleanly with no error output. -- When handling errors programmatically, read stderr, check the exit code, and re-run with `--verbose` to get a trace if you need to debug. - -## Structured outputs you can rely on - -| Command | Structured output | -| --------------------------------------------- | --------------------------------------------------------------------------------------------- | -| `clerk doctor --json` | `[{name, status, message, detail?, remedy?, fix?}]` | -| `clerk apps list --json` | Array of application objects | -| `clerk apps create --json` | Single application object | -| `clerk users list` (agent mode or `--json`) | `{data: [...users], hasMore}` envelope (BAPI user shape inside `data`) | -| `clerk users create` (agent mode or `--json`) | Single user object (raw BAPI shape) | -| `clerk users open` (agent mode) | `{url, appId, appName, instanceId, instanceLabel, userId, opened: false}` | -| `clerk api ` | Raw API JSON (Backend or Platform) on stdout | -| `clerk api --include` | Response headers on stderr, body on stdout | -| `clerk config pull` | Instance config JSON | -| `clerk config schema` | JSON Schema | -| `clerk open [subpath]` | `{url, appId, appName, instanceId, instanceLabel, subpath, opened: false}` (agent mode) | -| `clerk open --print` | Plain dashboard URL on stdout | -| `clerk deploy` (agent mode) | Deploy handoff report with `complete`, `state`, domain status, OAuth status, and `nextAction` | -| `clerk deploy status` (agent mode) | Deploy verification report with the same shape, plus exit `0` complete or `1` incomplete | -| Any command (agent mode) | On error: `{"error":{"code","message","docsUrl?","errors?"}}` on stderr | - -For commands without an explicit `--json` flag, `clerk api` is your escape hatch: hit the underlying endpoint directly. - -## Patterns for agent-driven use - -### Diagnose before acting - -```sh -clerk doctor --json --spotlight -``` - -Parse the output, then for each failing check read `remedy` and act. Never call `--fix` from an agent — it's interactive. - -In agent mode, `doctor` also includes a **`Host execution`** check when it can -detect that Clerk's host-side state is not writable. If that check warns, stop -trusting auth/link/env/API failures from the same sandboxed run and rerun the -relevant command on the host. - -### Preview every mutation - -```sh -# Dry run first -clerk api /users/user_abc123 -X DELETE --dry-run -# If the preview is what you expected, run it with --yes -clerk api /users/user_abc123 -X DELETE --yes -``` - -### Target explicitly - -```sh -# Don't rely on the linked profile for critical operations -clerk api /users --app app_abc123 --instance prod -``` - -The same advice applies to linking in agent mode: `clerk link --app app_abc123` is deterministic and works non-interactively. If you omit `--app`, the command only succeeds when silent autolink can prove the target app from existing publishable keys. - -### Deploy handoff and verification - -Do not try to drive the interactive deploy wizard from an agent. Use the handoff and check commands instead. - -```sh -# 1. Inspect current production deploy state without mutating anything. -clerk deploy --mode agent - -# 2. If the handoff says a human action is needed, ask the user to run this -# in a new terminal window, not through `! clerk deploy`: -clerk deploy --mode human - -# 3. After the user finishes or DNS has had time to propagate, verify: -clerk deploy status --mode agent - -# 4. If the user asks you to keep waiting, use the retrying wait loop: -clerk deploy status --mode agent --wait -``` - -`clerk deploy --mode agent` is read-only. It resolves the linked app and current production deploy snapshot, then emits JSON on stdout. It does **not** trigger DNS checks, poll, create production instances, patch OAuth config, or prompt. Linked projects exit `0` because this is an informational handoff. Not-linked and API failures still use the normal agent error envelope on stderr. - -Never try to run the human wizard through Claude's `! clerk deploy` shell escape or any non-interactive agent shell. The deploy wizard asks for domain, DNS export, OAuth, and verification inputs over stdin, so it needs a real human terminal. Tell the user to open a new terminal window in the project directory and run `clerk deploy` or `clerk deploy --mode human` there. After they finish, return to agent mode and run `clerk deploy status --mode agent`. - -`clerk deploy status --mode agent` is the gate. It is also read-only with respect to deploy configuration, but for an active production domain it triggers one Clerk DNS check, waits briefly, reads a live status/config snapshot, then reports DNS, SSL, email DNS, aggregate domain readiness, and OAuth completeness. By default it does not keep waiting or exponentially back off in agent mode. If the check is incomplete and the user asks the agent to continue waiting, run `clerk deploy status --mode agent --wait` instead of manually sleeping and retrying. `--wait` uses the shared poll loop: one immediate status read, then up to 5 exponential-backoff retries until aggregate domain status is complete. It emits the same status JSON. It exits: - -| Exit | Meaning | -| ---- | ------------------------------------------------------------------------------------ | -| `0` | Deploy is complete and verified. | -| `1` | The check ran successfully, but deploy is incomplete. Read `state` and `nextAction`. | -| else | A real CLI error occurred. Read the standard agent error envelope on stderr. | - -Deploy-specific agent errors still use the standard envelope and may include typed codes such as `plan_insufficient`, `provider_domain_not_allowed`, `home_url_taken`, or `form_param_invalid`. - -When a production instance exists, `nextAction` includes the full Clerk Dashboard domains URL so agents can send the user directly to the same page the human CLI prints in its next steps. Always show that URL to the user. Ask whether they want you to open it for them instead of omitting or paraphrasing it away. - -The deploy report has this shape: - -```json -{ - "complete": false, - "state": "domain_pending", - "domain": "example.com", - "productionInstanceId": "ins_...", - "domainStatus": { "dns": "complete", "ssl": "pending", "mail": "complete" }, - "pendingDnsRecords": [{ "type": "CNAME", "host": "clerk.example.com", "value": "..." }], - "oauth": { "complete": true, "configured": ["google"], "pending": [], "unsupported": [] }, - "nextAction": "SSL still provisioning for example.com. Re-run `clerk deploy status` in a few minutes, DNS propagation can take time. Ask the user to visit the Clerk Dashboard domains page, or offer to open it: https://dashboard.clerk.com/apps/app_.../instances/ins_.../domains" -} -``` - -`complete` is `true` only when the aggregate domain status is complete and all supported OAuth providers enabled in development have production credentials. The `domainStatus` object is a component summary; DNS, SSL, and email DNS can all read `complete` while `state` remains `domain_pending` if Clerk-side finalization is still pending. - -State precedence: - -| State | What to do | -| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `not_started` | Ask the human to run `clerk deploy --mode human`, then run `clerk deploy status --mode agent`. | -| `domain_provisioning` | Wait briefly or ask the human to finish `clerk deploy`, then run `clerk deploy status --mode agent`. | -| `domain_pending` | Surface `pendingDnsRecords` when present. Re-run `clerk deploy status --mode agent` after DNS, SSL, or email DNS propagation. | -| `oauth_pending` | Ask the human to finish the OAuth credential steps in `clerk deploy --mode human`, then verify with `deploy status`. | -| `complete` | No action needed. | - -Unsupported OAuth providers do not block `complete`, because the wizard cannot configure them automatically. They are still surfaced in `oauth.unsupported` so you can warn the user to review them in the Clerk Dashboard. - -### Use the catalog, not hard-coded paths - -```sh -clerk api ls users # discover available user endpoints -clerk api ls --platform apps # platform-side endpoints -``` - -### Surface doctor remedies to the user - -When `clerk doctor --json` reports a failure, show the user the `name`, `message`, and `remedy` — don't just silently try to fix it, because the underlying fix (e.g., `clerk auth login`) usually requires human interaction. - -`clerk doctor --fix` is disabled in agent mode, so you cannot rely on it. If a caller wants to attempt remediation anyway, map the failing check to the command that would fix it in human mode. Each check exposes this mapping via the optional `fix.label` field on the JSON result: - -| Failing check | Manual remediation | -| ----------------------- | -------------------------------------------- | -| `Logged in` | `clerk auth login` | -| `Authentication valid` | `clerk auth login` | -| `CLI configuration` | `clerk auth login` | -| `Project linked` | `clerk link` | -| `Application reachable` | `clerk link` | -| `Instance IDs` | `clerk link` | -| `Environment variables` | `clerk env pull` | -| `CLI version` | (no auto-fix; run `clerk update`) | -| `Shell completion` | (no auto-fix; see `clerk completion --help`) | - -All three remediation commands are themselves interactive by default: `auth login` opens a browser, `link` prompts for an app when `--app` is omitted, and `env pull` writes a file. In agent mode, prefer `clerk link --app ` over bare `clerk link`, since the bare form only works when silent autolink can resolve the target app without a picker. - -## What NOT to do in agent mode - -- **Don't ignore the sandbox warning.** If the CLI says host-only Clerk state or system capabilities may be unavailable, rerun the same command on the host before trusting the result. -- **Don't assume `clerk auth login` is fully unattended from an agent** — it opens a browser and waits for a callback. Prefer `CLERK_PLATFORM_API_KEY` for headless automation. `clerk init --app ` or init in an already linked project may still invoke the normal login fallback when a real app target is explicit. -- **Don't call `clerk link` without `--app` and assume the agent can pick for you** — it only succeeds when silent autolink can determine the app from detected keys. -- **Don't run `clerk unlink` in agent mode without `--yes`** — it exits with a usage error instead of prompting. -- **Don't run `clerk config put` without `--dry-run` first** — it's a full replacement and is destructive. -- **Don't skip `--yes` on mutations and expect them to work** — agent mode disables prompts, so commands that require confirmation will error. -- **Don't leak secret keys into logs** — the CLI never prints the raw secret key, and you shouldn't either. diff --git a/skills/clerk-cli/references/auth.md b/skills/clerk-cli/references/auth.md deleted file mode 100644 index 2e5e4160..00000000 --- a/skills/clerk-cli/references/auth.md +++ /dev/null @@ -1,155 +0,0 @@ -# Clerk CLI — Authentication & Targeting Reference - -Everything you need to know about how the CLI authenticates, resolves keys, and targets the right application/instance. - -## Two APIs, two auth paths - -Clerk exposes two HTTP APIs. The CLI speaks both. - -| API | Base URL | Auth | Used for | CLI flag | -| ------------------------ | --------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------ | -| **Backend API (BAPI)** | `https://api.clerk.dev/v1/` | Instance **secret key** (`sk_...`) | Tenant data: users, orgs, sessions, invitations, JWT templates, webhooks. | (default) | -| **Platform API (PLAPI)** | `https://api.clerk.com/v1/` | **Platform API key** (`ak_...`) or OAuth token from `clerk auth login` | Account-level: listing your applications, fetching app/instance metadata, pulling config, billing. | `--platform` | - -You override the base URLs via `CLERK_BACKEND_API_URL` and `CLERK_PLATFORM_API_URL` when testing against non-production Clerk environments. - -### Backend API secret key resolution order - -When you run `clerk api /users` (no `--platform`), the CLI picks the `sk_` key in this order: - -1. `--secret-key ` flag (explicit override) -2. `CLERK_SECRET_KEY` environment variable -3. Auto-resolved from `--app ` (uses `CLERK_PLATFORM_API_KEY` or stored OAuth token to fetch the app's secret key) -4. Auto-resolved from the linked project profile (same mechanism as #3, but the app ID comes from the repo's link) - -The CLI validates prefixes: passing `ak_...` where `sk_...` is expected (or vice versa) throws an error immediately with guidance on which key type to use. - -### Platform API auth resolution order - -When you run `clerk api --platform ...`, or any command that already uses PLAPI (`apps list`, `config pull`, `link`, etc.), the CLI picks the bearer token in this order: - -1. `CLERK_PLATFORM_API_KEY` environment variable -2. Stored OAuth token from `clerk auth login` -3. If neither is present, the CLI errors: "Not authenticated. Run `clerk auth login` or set `CLERK_PLATFORM_API_KEY`." - -Set `CLERK_PLATFORM_API_KEY` for CI and scripted agent usage. Use `clerk auth login` for local interactive development. - -## Host vs sandbox behavior - -These auth and targeting rules only produce trustworthy results when the CLI -can actually reach the user's host state. - -In agent mode, the CLI now emits a best-effort warning once per invocation -when it detects that host-only Clerk state or system capabilities are -unavailable: - -```text -Host-only Clerk state or system capabilities may be unavailable in agent mode. This may be a sandboxed run. -Re-run this command on the host shell before trusting auth, link, env, or API failures. -``` - -That warning usually means one of the following is blocked: - -- Clerk home-directory config or fallback credential files -- OS keychain access -- outbound Clerk network access -- browser launch or localhost OAuth callback setup - -When that warning appears, stop trusting the current invocation's auth or -targeting result. A sandboxed run can misreport: - -- `Not logged in` / `auth_required` -- `Not authenticated. Run clerk auth login or set CLERK_PLATFORM_API_KEY` -- `No Clerk project linked` -- missing env or missing linked profile state - -Rerun the same command on the host before acting on it. - -> **`config` commands do not accept `--secret-key`.** They target the Platform API and authenticate via the PLAPI chain above (`CLERK_PLATFORM_API_KEY` or the stored OAuth token). If you need to script `config pull/schema/patch/put` in CI, export `CLERK_PLATFORM_API_KEY`; a Backend API `sk_...` key will not work. - -## Project linking - -`clerk link` stores a mapping from your repo to a Clerk application in the CLI config file (run `clerk doctor --verbose` to see the resolved path; override with `CLERK_CONFIG_DIR`). The key is the normalized git remote URL (e.g., `github.com/org/repo`), which means the link is shared across all clones and worktrees of the same repo automatically. - -When you run a command without `--app`/`--instance`: - -1. The CLI resolves the current repo's profile (normalized git remote → git common dir → current working directory). -2. If linked, it uses the stored app ID and instance IDs. -3. If not linked, it errors with guidance to run `clerk link`. - -## `--app` and `--instance` targeting - -Most commands accept `--app ` and `--instance ` to override the linked profile: - -- `--app ` — Clerk application ID (starts with `app_`). Works from any directory; no link required. -- `--instance ` — One of: - - `dev` (development instance, the default) - - `prod` (production instance) - - a full instance ID (starts with `ins_`) - -Examples: - -```sh -# Operate on a specific app without linking the repo -clerk api /users --app app_abc123 - -# Pull production env keys (dangerous — only when you know what you're doing) -clerk env pull --app app_abc123 --instance prod - -# Target a specific instance directly -clerk config pull --instance ins_2aB3c... -``` - -## Auth commands - -### `clerk auth login` - -Aliases: `signup`, `signin`, `sign-in`. Top-level shortcut: `clerk login`. - -OAuth 2.0 PKCE flow against the Clerk OAuth system instance (`https://clerk.clerk.com` by default, overridable via `CLERK_OAUTH_BASE_URL`): - -1. Generates PKCE parameters. -2. Starts a local callback server on `127.0.0.1`. -3. Opens the browser to `/oauth/authorize`. -4. Exchanges the code at `/oauth/token` for an access token. -5. Fetches user info from `/oauth/userinfo`. -6. Stores the token in the OS credential store. - -In agent mode, if already authenticated, it's a no-op. If not, it prints guidance rather than opening a browser. -In a sandbox, even the "already authenticated" check can be false if the -keychain or fallback credential file is blocked, so rerun on the host before -trusting a sandboxed auth failure. - -### `clerk auth logout` - -Aliases: `signout`, `sign-out`. Top-level shortcut: `clerk logout`. - -Clears the stored token. No API calls. - -### `clerk whoami` - -Hits `GET /oauth/userinfo` with the stored token and prints the email. Exits with a message if not logged in. - -## Environment variables the CLI honors - -| Variable | Effect | -| ------------------------ | --------------------------------------------------------------- | -| `CLERK_MODE` | Force `human` or `agent` mode (overrides TTY detection). | -| `CLERK_SECRET_KEY` | BAPI secret key (bypasses linked project / `--app` resolution). | -| `CLERK_PLATFORM_API_KEY` | PLAPI bearer key. | -| `CLERK_BACKEND_API_URL` | Override Backend API base URL. | -| `CLERK_PLATFORM_API_URL` | Override Platform API base URL. | -| `CLERK_OAUTH_BASE_URL` | Override OAuth base URL (advanced / internal). | -| `CLERK_CONFIG_DIR` | Override config, cache, and credential directory (advanced). | - -## Common auth failure modes - -| Symptom | Likely cause | Fix | -| ----------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------ | -| `Not authenticated` | No token stored, no `CLERK_PLATFORM_API_KEY` | `clerk auth login` or export `CLERK_PLATFORM_API_KEY` | -| `No Clerk project linked` | Running a command that needs a linked profile with no `--app` | `clerk link` or pass `--app ` | -| `Invalid secret key prefix` | Passed `ak_...` where `sk_...` expected (or vice versa) | Check which API the command hits; pass the matching key type | -| `Unauthorized` from API | Key belongs to a different instance | Verify `--instance` and ensure the key matches | -| Sandbox warning + auth/link failure | Host-only Clerk state or system capabilities are blocked | Rerun the same command on the host before trusting the error | - -When in doubt: `clerk doctor --json` walks through all of this and tells you exactly what's wrong. diff --git a/skills/clerk-cli/references/recipes.md b/skills/clerk-cli/references/recipes.md deleted file mode 100644 index 4fe3930b..00000000 --- a/skills/clerk-cli/references/recipes.md +++ /dev/null @@ -1,299 +0,0 @@ -# Clerk CLI — Recipes - -Copy-pasteable patterns for common tasks. Treat these as starting points; confirm exact paths and parameters with `clerk api ls ` and `clerk --help`, since the Clerk API evolves. - -## Discovery first - -```sh -clerk api ls # everything Backend API exposes -clerk api ls users # filter by keyword -clerk api ls --platform # Platform API (account-level) -``` - -The bundled catalog is cached locally for 1 hour. There is no force-refresh flag — once the TTL expires the next `clerk api ls` re-fetches automatically; on fetch failure the CLI falls back to the stale cache and prints a warning. - -## Users - -```sh -# List users (preferred; curated flags). --limit defaults to 100 (max 250). -# JSON output is `{ data: [...], hasMore }` so callers can paginate without /users/count. -clerk users list -clerk users list --limit 50 --offset 0 --order-by -created_at - -# Count users (no curated subcommand; use the raw API) -clerk api /users/count - -# Fetch a user (no curated subcommand; use the raw API) -clerk api /users/user_abc123 - -# Search by email -clerk users list --email-address alice@example.com - -# Open a user's profile in the dashboard -clerk users open user_abc123 -clerk users open user_abc123 --print # print the URL instead of opening - -# Create a user (preferred; curated flags) -clerk users create \ - --email alice@example.com \ - --password 'SuperSecret123!' \ - --first-name Alice \ - --last-name Doe \ - --yes - -# Equivalent raw BAPI call. Use only when curated flags don't cover a field. -clerk api /users -d '{ - "email_address": ["alice@example.com"], - "password": "SuperSecret123!", - "first_name": "Alice", - "last_name": "Doe" -}' - -# Update (PATCH merges) -clerk api /users/user_abc123 -X PATCH -d '{"first_name":"Alicia"}' - -# Ban / unban -clerk api /users/user_abc123/ban -X POST -clerk api /users/user_abc123/unban -X POST - -# Lock / unlock -clerk api /users/user_abc123/lock -X POST -clerk api /users/user_abc123/unlock -X POST - -# Delete (PREVIEW FIRST) -clerk api /users/user_abc123 -X DELETE --dry-run -clerk api /users/user_abc123 -X DELETE --yes -``` - -### Test users (development only) - -For test accounts you need to sign into without real email or SMS delivery, Clerk provides two magic patterns that both verify with the fixed OTP `424242`. Use them on development instances; production rejects them. - -**By email.** Any address with the `+clerk_test` subaddress is recognized as a test email. The domain portion is arbitrary. - -```sh -# Create a test user with a test email (dev instance) -# `skip_password_checks` isn't a curated flag, so pass the body via `-d`. -clerk users create -d '{ - "email_address": ["demo+clerk_test@example.com"], - "password": "TestPass123!", - "skip_password_checks": true -}' --yes -``` - -**By phone.** Any US fictional phone number in the `+1 (XXX) 555-0100` through `+1 (XXX) 555-0199` range is recognized as a test phone. Pass the E.164 form. - -```sh -# Create a test user with a test phone (dev instance) -clerk users create -d '{ - "phone_number": ["+12015550100"], - "password": "TestPass123!", - "skip_password_checks": true -}' --yes -``` - -When signing in as either user in a browser or Playwright, enter `424242` at the OTP prompt. - -These patterns only apply to development instances. In production, client trust blocks sign-in regardless of suffix or number, and using real-looking test addresses is highly discouraged. Test addresses and numbers do not count against the dev-instance monthly caps (20 SMS, 100 emails). See [Clerk's test emails and phones reference](https://clerk.com/docs/guides/development/testing/test-emails-and-phones) for the full contract. - -## Organizations - -```sh -# List -clerk api /organizations -clerk api '/organizations?limit=20&query=acme' - -# Fetch -clerk api /organizations/org_abc123 - -# Create -clerk api /organizations -d '{"name":"Acme","created_by":"user_abc123"}' - -# Update -clerk api /organizations/org_abc123 -X PATCH -d '{"name":"Acme Inc."}' - -# Members -clerk api /organizations/org_abc123/memberships -clerk api /organizations/org_abc123/memberships -d '{"user_id":"user_xyz","role":"org:member"}' -clerk api /organizations/org_abc123/memberships/user_xyz -X PATCH -d '{"role":"org:admin"}' -clerk api /organizations/org_abc123/memberships/user_xyz -X DELETE --dry-run - -# Invitations -clerk api /organizations/org_abc123/invitations -d '{"email_address":"new@acme.com","role":"org:member"}' -``` - -If organization endpoints return `organization_not_enabled_in_instance`, enable the feature first: - -```sh -# Inspect org settings -clerk api /instance/organization_settings - -# Preview, then enable organizations for this instance -clerk api /instance/organization_settings -X PATCH -d '{"enabled":true}' --dry-run -clerk api /instance/organization_settings -X PATCH -d '{"enabled":true}' --yes -``` - -## Sessions - -```sh -# List active sessions for a user -clerk api '/sessions?user_id=user_abc123&status=active' - -# Revoke a session -clerk api /sessions/sess_abc123/revoke -X POST - -# Create an impersonation / sign-in token (for testing) -clerk api /sign_in_tokens -d '{"user_id":"user_abc123"}' -``` - -## Invitations (top-level, not org-scoped) - -```sh -clerk api /invitations -clerk api /invitations -d '{"email_address":"new@example.com","redirect_url":"https://example.com/welcome"}' -clerk api /invitations/inv_abc123/revoke -X POST -``` - -## JWT templates - -```sh -clerk api /jwt_templates -clerk api /jwt_templates/jtmp_abc123 -clerk api /jwt_templates -d '{ - "name": "supabase", - "claims": {"aud": "authenticated", "role": "authenticated"}, - "lifetime": 60 -}' -``` - -## Instance configuration - -Prefer the dedicated `config` commands over raw `api` calls — they handle confirmation, dry-run, and formatting. - -```sh -# Pull the current dev config -clerk config pull -clerk config pull --output config.dev.json - -# Pull production -clerk config pull --instance prod --output config.prod.json - -# Look at the schema to know what's available -clerk config schema --keys session sign_in social - -# PATCH: surgical updates -clerk config patch --json '{"session":{"lifetime":3600}}' --dry-run -clerk config patch --json '{"session":{"lifetime":3600}}' --yes - -# PUT: replace everything (destructive — always --dry-run first) -clerk config put --file config.prod.json --dry-run -clerk config put --file config.prod.json --instance prod --yes -``` - -## Environment variables - -```sh -# Pull dev keys into .env.local (auto-detects framework and key names) -clerk env pull - -# Pull production keys -clerk env pull --instance prod - -# Target a specific file -clerk env pull --file .env -``` - -`env pull` merges into the existing file: existing Clerk keys are updated in place; new ones are appended under a `# Clerk` header; everything else is preserved. - -## Applications (Platform API) - -```sh -# List your apps -clerk apps list -clerk apps list --json - -# Fetch one (raw API) -clerk api /v1/platform/applications/app_abc123 --platform -``` - -## Scripting patterns - -### Save large responses to a file before reading them - -`users list`, `apps list`, `config pull`, and most `clerk api` GETs can return responses ranging from kilobytes to megabytes. Reading the full payload into an LLM-driven session burns context for no benefit. Persist the response, then query just the slice you need: - -```sh -# Persist once, query as many times as you need. -clerk users list --json --limit 250 > /tmp/users.json - -jq '.data | length' /tmp/users.json # count rows on the page -jq '.hasMore' /tmp/users.json # any more pages? -jq '.data[0] | keys' /tmp/users.json # learn the shape of one record -jq '.data[] | {id, email_addresses}' /tmp/users.json # project to relevant fields only -``` - -If `jq` is not on `PATH`, fall back to Python or Node, which most environments have: - -```sh -python3 -c 'import json; d=json.load(open("/tmp/users.json")); print(len(d["data"]), d["hasMore"])' -node -e 'const d=require("/tmp/users.json"); console.log(d.data.length, d.hasMore)' -``` - -Only `cat`/`head` the file when you genuinely need the raw structure for one-off debugging. - -### Pipe to `jq` - -For small responses (or one-shot lookups), inline piping to `jq` is fine: - -```sh -# Get a list of user IDs from the current page (the page envelope is `{ data, hasMore }`) -clerk users list --json | jq -r '.data[] | .id' - -# Count banned users on the current page -clerk users list --json | jq '[.data[] | select(.banned)] | length' - -# Walk every page until hasMore is false. Save each page to its own file so you -# can inspect them independently without re-fetching. -offset=0 -while :; do - page="/tmp/users-${offset}.json" - clerk users list --json --limit 250 --offset "$offset" > "$page" - jq -r '.data[] | .id' "$page" - [ "$(jq -r '.hasMore' "$page")" = "true" ] || break - offset=$((offset + 250)) -done -``` - -### Read body from stdin - -```sh -echo '{"first_name":"Bob"}' | clerk api /users/user_abc123 -X PATCH -jq -n '{email_address:["c@d.co"]}' | clerk api /users -``` - -### Loop safely - -```sh -# Always --dry-run first across the whole set. `users list` paginates; -# bump --limit (max 250) and walk pages with --offset until .hasMore is false. -for id in $(clerk users list --json --limit 250 | jq -r '.data[] | .id'); do - clerk api /users/$id -X PATCH -d '{"public_metadata":{"migrated":true}}' --dry-run -done -# Re-run without --dry-run once the previews look right -``` - -### Target multiple instances - -```sh -# Copy config from dev to staging for review -clerk config pull --instance dev --output /tmp/dev-config.json -clerk config patch --instance ins_staging --file /tmp/dev-config.json --dry-run -``` - -## When in doubt - -```sh -clerk api ls # find the right endpoint -clerk --help # authoritative flag list -clerk doctor --json # health check -``` From 6340f2dbc4ef14071d601688692d1071b6c48a16 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 1 Jun 2026 11:34:47 -0400 Subject: [PATCH 2/5] refactor: move skills installer helpers to lib Share skills CLI installation helpers from lib now that the standalone skill command directory is gone. --- .../cli-core/src/commands/billing/index.test.ts | 2 +- packages/cli-core/src/commands/billing/index.ts | 2 +- packages/cli-core/src/commands/init/skills.ts | 2 +- .../skill/install.test.ts => lib/skills.test.ts} | 2 +- .../{commands/skill/install.ts => lib/skills.ts} | 14 +++++++------- 5 files changed, 11 insertions(+), 11 deletions(-) rename packages/cli-core/src/{commands/skill/install.test.ts => lib/skills.test.ts} (97%) rename packages/cli-core/src/{commands/skill/install.ts => lib/skills.ts} (91%) diff --git a/packages/cli-core/src/commands/billing/index.test.ts b/packages/cli-core/src/commands/billing/index.test.ts index ada24d92..088ce5fb 100644 --- a/packages/cli-core/src/commands/billing/index.test.ts +++ b/packages/cli-core/src/commands/billing/index.test.ts @@ -31,7 +31,7 @@ function resetSkillStubs() { skillCalls.length = 0; resolveSkillsRunnerStub = () => ({ id: "bunx", display: "bunx" }); } -mock.module("../skill/install.ts", () => ({ +mock.module("../../lib/skills.ts", () => ({ resolveSkillsRunner: async () => resolveSkillsRunnerStub(), runSkillsAdd: async ( _runner: unknown, diff --git a/packages/cli-core/src/commands/billing/index.ts b/packages/cli-core/src/commands/billing/index.ts index 2d8af333..125ae09c 100644 --- a/packages/cli-core/src/commands/billing/index.ts +++ b/packages/cli-core/src/commands/billing/index.ts @@ -5,8 +5,8 @@ import { log } from "../../lib/log.ts"; import { confirm } from "../../lib/prompts.ts"; import { detectPackageManager } from "../../lib/package-manager.ts"; import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts"; +import { resolveSkillsRunner, runSkillsAdd } from "../../lib/skills.ts"; import { applyConfigPatch } from "../config/apply-patch.ts"; -import { resolveSkillsRunner, runSkillsAdd } from "../skill/install.ts"; interface BillingOptions { app?: string; diff --git a/packages/cli-core/src/commands/init/skills.ts b/packages/cli-core/src/commands/init/skills.ts index f13bce5a..c73e33cd 100644 --- a/packages/cli-core/src/commands/init/skills.ts +++ b/packages/cli-core/src/commands/init/skills.ts @@ -15,8 +15,8 @@ import { isHuman } from "../../mode.js"; import { log } from "../../lib/log.js"; import { confirm } from "../../lib/prompts.js"; +import { resolveSkillsRunner, runSkillsAdd } from "../../lib/skills.js"; import type { ProjectContext } from "./frameworks/types.js"; -import { resolveSkillsRunner, runSkillsAdd } from "../skill/install.js"; /** Upstream skills from clerk/skills — installed on every project. */ const DEFAULT_UPSTREAM_SKILLS = [ diff --git a/packages/cli-core/src/commands/skill/install.test.ts b/packages/cli-core/src/lib/skills.test.ts similarity index 97% rename from packages/cli-core/src/commands/skill/install.test.ts rename to packages/cli-core/src/lib/skills.test.ts index b14c7319..bd0fdeb7 100644 --- a/packages/cli-core/src/commands/skill/install.test.ts +++ b/packages/cli-core/src/lib/skills.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from "bun:test"; -import { buildSkillsArgs } from "./install.ts"; +import { buildSkillsArgs } from "./skills.ts"; describe("buildSkillsArgs", () => { const skills = ["clerk", "clerk-setup", "clerk-nextjs-patterns"]; diff --git a/packages/cli-core/src/commands/skill/install.ts b/packages/cli-core/src/lib/skills.ts similarity index 91% rename from packages/cli-core/src/commands/skill/install.ts rename to packages/cli-core/src/lib/skills.ts index 56e107a7..da05cd1a 100644 --- a/packages/cli-core/src/commands/skill/install.ts +++ b/packages/cli-core/src/lib/skills.ts @@ -7,18 +7,18 @@ * `-y -g` so it runs unattended with global scope and auto-detected agents. */ -import { dim } from "../../lib/color.js"; -import { log } from "../../lib/log.js"; -import { select } from "../../lib/listage.js"; +import { dim } from "./color.js"; +import { log } from "./log.js"; +import { select } from "./listage.js"; import { type Runner, detectAvailableRunners, preferredRunner, runnerCommand, runnerForPackageManager, -} from "../../lib/runners.js"; -import { isNonEmpty } from "../../lib/helpers/arrays.js"; -import type { PackageManager } from "../../lib/package-manager.js"; +} from "./runners.js"; +import { isNonEmpty } from "./helpers/arrays.js"; +import type { PackageManager } from "./package-manager.js"; /** * Build the runner-agnostic argv for `skills add ...`. The caller @@ -53,7 +53,7 @@ export function buildSkillsArgs( /** * Run a single `skills add ...` invocation. Returns true on success, false * on any failure (spawn error, non-zero exit). Failures print a yellow - * warning but never throw — skills are optional and shouldn't tear down + * warning but never throw, skills are optional and shouldn't tear down * a successful scaffold. */ export async function runSkillsAdd( From 0eb758acaf630cdd4bfe2e2444b6be8054e86464 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 1 Jun 2026 12:37:55 -0400 Subject: [PATCH 3/5] docs(init): mention cli skill in install flow --- packages/cli-core/src/commands/init/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index 45c7998f..91d13366 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -67,7 +67,7 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful 13. Prints a summary of created, modified, and skipped files with recommendations 14. **Authenticated mode**: pulls development instance API keys via `clerk env pull` 15. **Unauthenticated mode**: prints instructions for development without API keys and how to connect a Clerk account later -16. 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)) +16. Optionally installs Clerk agent skills (cli + core + features, plus a framework-specific skill) via the project's package runner (see [Agent skills install](#agent-skills-install)) ## Framework Detection From a2365924b4827a2b2d238e6202bee474089ad572 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 1 Jun 2026 12:46:21 -0400 Subject: [PATCH 4/5] Update packages/cli-core/src/lib/skills.ts Co-authored-by: Rafael Thayto --- packages/cli-core/src/lib/skills.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli-core/src/lib/skills.ts b/packages/cli-core/src/lib/skills.ts index da05cd1a..4c9117a2 100644 --- a/packages/cli-core/src/lib/skills.ts +++ b/packages/cli-core/src/lib/skills.ts @@ -46,8 +46,7 @@ export function buildSkillsArgs( ): string[] { const skillFlags = skillNames.flatMap((s) => ["--skill", s]); const extraFlags = interactive ? [] : ["-y", "-g"]; - const copyFlag = copy ? ["--copy"] : []; - return ["skills", "add", source, ...skillFlags, ...extraFlags, ...copyFlag]; + return ["skills", "add", source, ...skillFlags, ...extraFlags]; } /** From 64b406d214447ec83f8fa3ef8dae3e9d8eef3e09 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 1 Jun 2026 13:00:11 -0400 Subject: [PATCH 5/5] fix(skills): remove dead copy option --- .../cli-core/src/commands/billing/index.ts | 1 - packages/cli-core/src/commands/init/skills.ts | 1 - packages/cli-core/src/lib/skills.test.ts | 18 +++++------------- packages/cli-core/src/lib/skills.ts | 7 +------ 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/cli-core/src/commands/billing/index.ts b/packages/cli-core/src/commands/billing/index.ts index 125ae09c..4e07ec4b 100644 --- a/packages/cli-core/src/commands/billing/index.ts +++ b/packages/cli-core/src/commands/billing/index.ts @@ -109,7 +109,6 @@ async function offerBillingSkillInstall(options: BillingOptions): Promise "clerk/skills", ["clerk-billing"], interactive, - false, "clerk-billing", ); if (installed) { diff --git a/packages/cli-core/src/commands/init/skills.ts b/packages/cli-core/src/commands/init/skills.ts index c73e33cd..d460b631 100644 --- a/packages/cli-core/src/commands/init/skills.ts +++ b/packages/cli-core/src/commands/init/skills.ts @@ -107,7 +107,6 @@ export async function installSkills( UPSTREAM_SKILLS_SOURCE, upstreamSkills, interactive, - false, formatSkillsSummary(frameworkSkill), ); diff --git a/packages/cli-core/src/lib/skills.test.ts b/packages/cli-core/src/lib/skills.test.ts index bd0fdeb7..12396dd7 100644 --- a/packages/cli-core/src/lib/skills.test.ts +++ b/packages/cli-core/src/lib/skills.test.ts @@ -6,7 +6,7 @@ describe("buildSkillsArgs", () => { const upstream = "clerk/skills"; test("interactive mode: no -y or -g, lets skills CLI take over", () => { - const args = buildSkillsArgs(upstream, skills, true, false); + const args = buildSkillsArgs(upstream, skills, true); expect(args).toEqual([ "skills", "add", @@ -25,28 +25,20 @@ describe("buildSkillsArgs", () => { }); test("non-interactive mode: includes -y and -g for global auto-detect", () => { - const args = buildSkillsArgs(upstream, skills, false, false); + const args = buildSkillsArgs(upstream, skills, false); expect(args).toContain("-y"); expect(args).toContain("-g"); expect(args).not.toContain("--agent"); }); test("never passes --agent (lets skills CLI auto-detect)", () => { - expect(buildSkillsArgs(upstream, skills, true, false)).not.toContain("--agent"); - expect(buildSkillsArgs(upstream, skills, false, false)).not.toContain("--agent"); + expect(buildSkillsArgs(upstream, skills, true)).not.toContain("--agent"); + expect(buildSkillsArgs(upstream, skills, false)).not.toContain("--agent"); }); test("empty skillNames omits --skill flags", () => { - const args = buildSkillsArgs("clerk/skills", [], true, false); + const args = buildSkillsArgs("clerk/skills", [], true); expect(args).toEqual(["skills", "add", "clerk/skills"]); expect(args).not.toContain("--skill"); }); - - test("copy=true appends --copy flag", () => { - const args = buildSkillsArgs("clerk/skills", [], false, true); - expect(args).toContain("--copy"); - // --copy should trail -y / -g, not replace them. - expect(args).toContain("-y"); - expect(args).toContain("-g"); - }); }); diff --git a/packages/cli-core/src/lib/skills.ts b/packages/cli-core/src/lib/skills.ts index 4c9117a2..5de61f8c 100644 --- a/packages/cli-core/src/lib/skills.ts +++ b/packages/cli-core/src/lib/skills.ts @@ -28,9 +28,6 @@ import type { PackageManager } from "./package-manager.js"; * `skillNames` becomes `--skill ` pairs; leave empty to install every * skill from `source`. * - * `copy` forces the `skills` CLI to copy files into each agent dir instead - * of symlinking. - * * Interactive mode: hand off to the skills CLI's native UX (auto-detect * installed agents, scope picker) by omitting `--agent` and `-y`. * Non-interactive: pass `-y -g` so it runs unattended with global scope @@ -42,7 +39,6 @@ export function buildSkillsArgs( source: string, skillNames: readonly string[], interactive: boolean, - copy: boolean, ): string[] { const skillFlags = skillNames.flatMap((s) => ["--skill", s]); const extraFlags = interactive ? [] : ["-y", "-g"]; @@ -61,10 +57,9 @@ export async function runSkillsAdd( source: string, skillNames: readonly string[], interactive: boolean, - copy: boolean, label: string, ): Promise { - const command = runnerCommand(runner, buildSkillsArgs(source, skillNames, interactive, copy)); + const command = runnerCommand(runner, buildSkillsArgs(source, skillNames, interactive)); const displayCommand = `${runner.display} skills add ${source}`; log.blank();