From 7fe032926e11e1d4e41ff6ab2a80cd7383a85194 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Tue, 21 Apr 2026 12:37:50 -0300 Subject: [PATCH 1/3] feat(help): suppress skill-install tip when the clerk skill is already installed Detect common local-agent skill paths (Claude Code, Codex, Cursor, Windsurf, Zed, Cline, VS Code, GitHub Copilot) and omit the "install the Clerk skills" block from `clerk --help` and bare `clerk` output when a clerk skill is already present. Check is synchronous via `fs.existsSync` so it fits Commander's help-rendering path; a missed match just keeps the tip visible. --- .changeset/skip-skill-tip-when-installed.md | 5 ++ packages/cli-core/src/cli-program.test.ts | 94 +++++++++++++++++++- packages/cli-core/src/cli-program.ts | 8 +- packages/cli-core/src/lib/skill-detection.ts | 52 +++++++++++ 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 .changeset/skip-skill-tip-when-installed.md create mode 100644 packages/cli-core/src/lib/skill-detection.ts diff --git a/.changeset/skip-skill-tip-when-installed.md b/.changeset/skip-skill-tip-when-installed.md new file mode 100644 index 00000000..44b83658 --- /dev/null +++ b/.changeset/skip-skill-tip-when-installed.md @@ -0,0 +1,5 @@ +--- +"clerk": patch +--- + +Hide the "install the Clerk skills" tip in `clerk --help` and bare `clerk` output when the Clerk agent skill is already installed for one of the common local agents (Claude Code, Codex, Cursor, Windsurf, Zed, Cline, VS Code, GitHub Copilot). diff --git a/packages/cli-core/src/cli-program.test.ts b/packages/cli-core/src/cli-program.test.ts index b79d4258..e292fc60 100644 --- a/packages/cli-core/src/cli-program.test.ts +++ b/packages/cli-core/src/cli-program.test.ts @@ -1,5 +1,8 @@ -import { test, expect, describe } from "bun:test"; -import { formatApiBody } from "./cli-program.ts"; +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 { createProgram, formatApiBody } from "./cli-program.ts"; describe("formatApiBody", () => { // --- Single error with meta --- @@ -200,3 +203,90 @@ describe("formatApiBody", () => { expect(result).toBe("Plan limitation"); }); }); + +describe("help: clerk skill install tip", () => { + const TIP_SUBSTR = "Give AI agents better Clerk context"; + const STANDARD_AGENT_DIRS = [ + ".claude", + ".agents", + ".codex", + ".cursor", + ".windsurf", + ".zed", + ".cline", + ]; + const EXTRA_REL_PATHS = [".vscode/skills/clerk/SKILL.md", ".github/prompts/clerk.md"]; + + // 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"); + }); + + for (const dir of STANDARD_AGENT_DIRS) { + test(`hides the tip when ${dir}/skills/clerk/SKILL.md exists under HOME`, () => { + const target = join(tmpHome, dir, "skills/clerk"); + mkdirSync(target, { recursive: true }); + writeFileSync(join(target, "SKILL.md"), "ok"); + expect(renderHelp()).not.toContain(TIP_SUBSTR); + }); + + test(`hides the tip when ${dir}/skills/clerk/SKILL.md exists under cwd`, () => { + const target = join(tmpCwd, dir, "skills/clerk"); + mkdirSync(target, { recursive: true }); + writeFileSync(join(target, "SKILL.md"), "ok"); + expect(renderHelp()).not.toContain(TIP_SUBSTR); + }); + } + + for (const rel of EXTRA_REL_PATHS) { + test(`hides the tip when ${rel} exists under cwd`, () => { + 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 c480d4b6..1d6b18ef 100644 --- a/packages/cli-core/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -42,6 +42,7 @@ import { isAgent } from "./mode.ts"; import { log } from "./lib/log.ts"; import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts"; import { update } from "./commands/update/index.ts"; +import { isClerkSkillInstalled } from "./lib/skill-detection.ts"; export function createProgram() { const program = new Command() @@ -56,9 +57,10 @@ export function createProgram() { "Force interaction mode (human or agent). Defaults to auto-detect based on TTY.", ) .option("--verbose", "Show detailed output (enables debug messages)") - .addHelpText( - "after", - ` + .addHelpText("after", () => + isClerkSkillInstalled() + ? "" + : ` Give AI agents better Clerk context: install the Clerk skills $ clerk skill install`, ); diff --git a/packages/cli-core/src/lib/skill-detection.ts b/packages/cli-core/src/lib/skill-detection.ts new file mode 100644 index 00000000..43e6b86f --- /dev/null +++ b/packages/cli-core/src/lib/skill-detection.ts @@ -0,0 +1,52 @@ +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/SKILL.md` (global) and + * `//skills/clerk/SKILL.md` (project-local). + */ +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/SKILL.md"; + +/** + * Paths for agents that don't follow the uniform + * `/skills//SKILL.md` layout. Checked as-is under both + * `$HOME` and ``. + */ +const EXTRA_REL_PATHS = [ + ".vscode/skills/clerk/SKILL.md", // VS Code (project-local) + ".github/prompts/clerk.md", // GitHub Copilot (project-local) +] as const; + +/** + * Best-effort synchronous check for whether the bundled `clerk` 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(home, rel))) return true; + if (existsSync(join(cwd, rel))) return true; + } + return false; +} From fdd3081fc9f5376078570c8c12626e3e654105f8 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Wed, 22 Apr 2026 14:02:16 -0300 Subject: [PATCH 2/3] fix(skill-detection): restrict EXTRA_REL_PATHS checks to cwd only EXTRA_REL_PATHS (.vscode/skills, .github/prompts) are project-local markers that `clerk skill install` never places under $HOME. Checking them under $HOME could cause a stray file to suppress the install tip globally. Remove the `join(home, rel)` check so these paths are only matched under the current working directory. --- packages/cli-core/src/lib/skill-detection.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli-core/src/lib/skill-detection.ts b/packages/cli-core/src/lib/skill-detection.ts index 43e6b86f..1d436ca3 100644 --- a/packages/cli-core/src/lib/skill-detection.ts +++ b/packages/cli-core/src/lib/skill-detection.ts @@ -22,8 +22,9 @@ const STANDARD_SKILL_REL = "skills/clerk/SKILL.md"; /** * Paths for agents that don't follow the uniform - * `/skills//SKILL.md` layout. Checked as-is under both - * `$HOME` and ``. + * `/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. */ const EXTRA_REL_PATHS = [ ".vscode/skills/clerk/SKILL.md", // VS Code (project-local) @@ -45,7 +46,6 @@ export function isClerkSkillInstalled(): boolean { if (existsSync(join(cwd, dir, STANDARD_SKILL_REL))) return true; } for (const rel of EXTRA_REL_PATHS) { - if (existsSync(join(home, rel))) return true; if (existsSync(join(cwd, rel))) return true; } return false; From d1e22615b817397448a2e66a894b9d667275243c Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Wed, 22 Apr 2026 14:21:11 -0300 Subject: [PATCH 3/3] refactor(skill-detection): export constants and import in tests Export STANDARD_AGENT_DIRS and EXTRA_REL_PATHS from skill-detection.ts so the test file can import them instead of duplicating the values. --- packages/cli-core/src/cli-program.test.ts | 11 +---------- packages/cli-core/src/lib/skill-detection.ts | 4 ++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/cli-core/src/cli-program.test.ts b/packages/cli-core/src/cli-program.test.ts index e292fc60..e1ee982d 100644 --- a/packages/cli-core/src/cli-program.test.ts +++ b/packages/cli-core/src/cli-program.test.ts @@ -3,6 +3,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { createProgram, formatApiBody } from "./cli-program.ts"; +import { STANDARD_AGENT_DIRS, EXTRA_REL_PATHS } from "./lib/skill-detection.ts"; describe("formatApiBody", () => { // --- Single error with meta --- @@ -206,16 +207,6 @@ describe("formatApiBody", () => { describe("help: clerk skill install tip", () => { const TIP_SUBSTR = "Give AI agents better Clerk context"; - const STANDARD_AGENT_DIRS = [ - ".claude", - ".agents", - ".codex", - ".cursor", - ".windsurf", - ".zed", - ".cline", - ]; - const EXTRA_REL_PATHS = [".vscode/skills/clerk/SKILL.md", ".github/prompts/clerk.md"]; // Capture help output including `addHelpText("after", ...)`. The custom // formatter in lib/help.ts rebuilds help from scratch, so the "after" diff --git a/packages/cli-core/src/lib/skill-detection.ts b/packages/cli-core/src/lib/skill-detection.ts index 1d436ca3..6569c441 100644 --- a/packages/cli-core/src/lib/skill-detection.ts +++ b/packages/cli-core/src/lib/skill-detection.ts @@ -8,7 +8,7 @@ import { join } from "node:path"; * both `$HOME//skills/clerk/SKILL.md` (global) and * `//skills/clerk/SKILL.md` (project-local). */ -const STANDARD_AGENT_DIRS = [ +export const STANDARD_AGENT_DIRS = [ ".claude", // Claude Code ".agents", // generic / shared ".codex", // OpenAI Codex CLI @@ -26,7 +26,7 @@ const STANDARD_SKILL_REL = "skills/clerk/SKILL.md"; * and are checked under `` (not `$HOME`), because * `clerk skill install` does not install these layouts globally. */ -const EXTRA_REL_PATHS = [ +export const EXTRA_REL_PATHS = [ ".vscode/skills/clerk/SKILL.md", // VS Code (project-local) ".github/prompts/clerk.md", // GitHub Copilot (project-local) ] as const;