four-leaf-coach 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/SKILL.md +54 -0
  4. package/bin/four-leaf-coach.js +439 -0
  5. package/dist/claude-code/.claude/skills/four-leaf-coach/SKILL.md +54 -0
  6. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/analyze-jd.md +44 -0
  7. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/find-jobs.md +41 -0
  8. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/interview-strategy.md +45 -0
  9. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/kickoff.md +41 -0
  10. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/negotiate-prep.md +80 -0
  11. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/practice.md +49 -0
  12. package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/prep-role.md +43 -0
  13. package/dist/claude-code/.claude/skills/four-leaf-coach/references/mcp-tools.md +57 -0
  14. package/dist/claude-code/.claude/skills/four-leaf-coach/references/upgrade-flow.md +38 -0
  15. package/dist/codex/AGENTS.md +54 -0
  16. package/dist/codex/references/commands/analyze-jd.md +44 -0
  17. package/dist/codex/references/commands/find-jobs.md +41 -0
  18. package/dist/codex/references/commands/interview-strategy.md +45 -0
  19. package/dist/codex/references/commands/kickoff.md +41 -0
  20. package/dist/codex/references/commands/negotiate-prep.md +80 -0
  21. package/dist/codex/references/commands/practice.md +49 -0
  22. package/dist/codex/references/commands/prep-role.md +43 -0
  23. package/dist/codex/references/mcp-tools.md +57 -0
  24. package/dist/codex/references/upgrade-flow.md +38 -0
  25. package/dist/cursor/.cursor/skills/four-leaf-coach/SKILL.md +54 -0
  26. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/analyze-jd.md +44 -0
  27. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/find-jobs.md +41 -0
  28. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/interview-strategy.md +45 -0
  29. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/kickoff.md +41 -0
  30. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/negotiate-prep.md +80 -0
  31. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/practice.md +49 -0
  32. package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/prep-role.md +43 -0
  33. package/dist/cursor/.cursor/skills/four-leaf-coach/references/mcp-tools.md +57 -0
  34. package/dist/cursor/.cursor/skills/four-leaf-coach/references/upgrade-flow.md +38 -0
  35. package/dist/github/.github/copilot-instructions.md +516 -0
  36. package/package.json +46 -0
  37. package/references/commands/analyze-jd.md +44 -0
  38. package/references/commands/find-jobs.md +41 -0
  39. package/references/commands/interview-strategy.md +45 -0
  40. package/references/commands/kickoff.md +41 -0
  41. package/references/commands/negotiate-prep.md +80 -0
  42. package/references/commands/practice.md +49 -0
  43. package/references/commands/prep-role.md +43 -0
  44. package/references/mcp-tools.md +57 -0
  45. package/references/upgrade-flow.md +38 -0
  46. package/scripts/build.js +229 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Four-leaf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # four-leaf-coach
2
+
3
+ An open-source Skill that turns Claude (or ChatGPT, Cursor, Codex, GitHub Copilot) into a job search and interview prep coach. It pulls real job postings, role-specific interview intelligence, and resume scoring from the hosted Four-Leaf MCP, then walks the user through preparing for an actual interview.
4
+
5
+ Free to install and use. Voice mock interviews with rubric-scored feedback and full AI resume tailoring live on [four-leaf.ai](https://four-leaf.ai); the Skill surfaces those as an upgrade path when they're the right next step.
6
+
7
+ ## What it does
8
+
9
+ Walks a user through prep for a specific role at a specific company. The Skill greets, asks what they're prepping for, and routes them into one of seven guided workflows. Every workflow pulls live data from the Four-Leaf MCP (jobs, role intel, question bank, match scoring) and adds the Skill's coaching on top.
10
+
11
+ The seven commands:
12
+
13
+ - `kickoff` figures out what the user is prepping for and routes them
14
+ - `find-jobs <query>` runs natural language search across 100k+ active postings
15
+ - `prep-role <role> [company] [seniority]` covers interview pipeline, what to expect, how to win
16
+ - `practice <role> [type] [difficulty]` generates calibrated questions and coaches answers
17
+ - `analyze-jd` scores a resume against a JD and points out gaps
18
+ - `negotiate-prep` walks through a compensation negotiation framework
19
+ - `interview-strategy <topic>` covers formats, AI interviewers, work trials, signal vs noise
20
+
21
+ ## Install
22
+
23
+ Two steps. The Skill tells your AI tool how to coach. The hosted MCP gives the Skill live data.
24
+
25
+ ### Step 1: install the Skill
26
+
27
+ ```bash
28
+ npx four-leaf-coach add
29
+ ```
30
+
31
+ That's it. The CLI detects your tool (Claude Code, Cursor, Codex, or GitHub Copilot), asks for scope when it matters, and copies the right bundle into the right place. Useful flags:
32
+
33
+ - `--tool <name>` pick the tool yourself: `claude-code`, `cursor`, `codex`, or `github-copilot`
34
+ - `--scope <project|global>` Claude Code only; where to install
35
+ - `--dry-run` show what it would do without writing anything
36
+ - `--yes` skip confirmation prompts, `--force` overwrite an existing install
37
+ - `four-leaf-coach list` show the supported tools and what's detected in the current directory
38
+
39
+ Cursor needs the Nightly channel with **Settings, Rules, Agent Skills** enabled. The other tools work out of the box.
40
+
41
+ ### Step 2: install the Four-Leaf MCP for live data
42
+
43
+ The Skill works in degraded mode (coaching only, no live job data) without the MCP. To get real job search, role intel, and resume scoring, install the hosted MCP:
44
+
45
+ ```bash
46
+ # Claude Code, Claude Desktop, and any tool that uses claude-mcp config
47
+ claude mcp add --transport http four-leaf https://four-leaf.ai/api/mcp
48
+ ```
49
+
50
+ The first tool call opens the browser for OAuth. A free Four-Leaf account works (3-day trial included, no credit card).
51
+
52
+ For Cursor, ChatGPT Desktop, and other MCP-aware tools, configure the same URL (`https://four-leaf.ai/api/mcp`) per that tool's MCP setup docs.
53
+
54
+ ### Step 3: use it
55
+
56
+ In your tool, type `/kickoff` (or `kickoff` if your tool doesn't use slash commands). The coach takes it from there.
57
+
58
+ ## Manual install
59
+
60
+ If you'd rather not run `npx` (air-gapped network, or you just want to see what lands where), clone and build the bundles yourself. This is exactly what `npx four-leaf-coach add --tool <name>` automates.
61
+
62
+ ```bash
63
+ git clone https://github.com/fourleafai/clover-public.git
64
+ cd clover-public
65
+ npm run build
66
+ ```
67
+
68
+ `npm run build` reads `SKILL.md` plus the `references/` tree and writes a `dist/<tool>/` directory for each supported tool. Then copy your tool's bundle into place:
69
+
70
+ | Tool | Build output | Copy into place |
71
+ |---|---|---|
72
+ | Claude Code (global, all projects) | `dist/claude-code/` | `cp -r dist/claude-code/.claude ~/` |
73
+ | Claude Code (single project) | `dist/claude-code/` | `cp -r dist/claude-code/.claude PROJECT/` (replace `PROJECT` with your project path) |
74
+ | Cursor (Nightly + Agent Skills enabled) | `dist/cursor/` | `cp -r dist/cursor/.cursor PROJECT/` |
75
+ | OpenAI Codex CLI | `dist/codex/` | `cp -r dist/codex/AGENTS.md dist/codex/references PROJECT/`, then run Codex from `PROJECT` |
76
+ | GitHub Copilot | `dist/github/` | `cp -r dist/github/.github PROJECT/` (flattened single-file variant) |
77
+
78
+ The GitHub Copilot bundle is a single flattened file (`.github/copilot-instructions.md`) because Copilot reads one instructions file and doesn't follow references. The build inlines the whole Skill for it. The other three tools follow file references, so they get the source tree as-is.
79
+
80
+ ## What's free vs paid
81
+
82
+ - **Free**: the Skill itself, all the data tools in the MCP (jobs, role intel, question bank, match scoring). Daily rate limits on a few of the compute-heavier tools.
83
+ - **Paid on [four-leaf.ai](https://four-leaf.ai)**: voice mock interviews with adaptive AI follow-ups and rubric-scored feedback per answer, full AI resume tailoring against a specific JD, application tracking. Three options at [four-leaf.ai/pricing](https://four-leaf.ai/pricing). 3-day free trial (no card), $5 5-Day Pass, $20/mo Pro. All three give you the same features.
84
+
85
+ The Skill surfaces these when they're the right next step. It doesn't push.
86
+
87
+ ## FAQ
88
+
89
+ **What is four-leaf-coach?**
90
+ An open-source Skill that turns Claude, Cursor, OpenAI Codex, or GitHub Copilot into a job-search and interview-prep coach. The Skill is a structured set of instructions plus reference files that your AI tool loads. It works with the hosted Four-Leaf MCP server for live data (real job postings, role-specific interview intelligence, resume scoring).
91
+
92
+ **How is this different from just prompting Claude for interview prep?**
93
+ Generic prompts produce generic advice. four-leaf-coach calls real tools: `search_jobs` returns actual apply URLs from 100k+ active postings; `match_score` runs a real scoring algorithm against your resume; `generate_practice_questions` produces role-calibrated questions; `get_interview_questions` pulls from a curated question bank. The Skill orchestrates the calls and coaches around them.
94
+
95
+ **Do I need a Four-Leaf account?**
96
+ For the data tools (jobs, role intel, question bank, match scoring) a free Four-Leaf account works. The 3-day trial requires no credit card. Voice mock interviews with rubric-scored feedback per answer and full AI resume tailoring are paid features on four-leaf.ai. The Skill surfaces those as an upgrade path when relevant.
97
+
98
+ **Does the MCP work with ChatGPT?**
99
+ Yes. The MCP is HTTP-based with OAuth, so any MCP-aware client connects: Claude Desktop, Claude Code, Cursor, ChatGPT Desktop (Plus + Dev mode), Cline, Continue, Windsurf. The Skill itself is a Claude / Cursor / Codex / Copilot primitive; ChatGPT doesn't yet have a comparable file-based Skill convention, but the underlying MCP tools work there.
100
+
101
+ **Can I use the Skill without the MCP?**
102
+ Yes, in degraded mode. Without the MCP, the Skill still coaches with structured workflows for prep, practice, JD analysis, and negotiation, just without live job listings or real resume scoring. Install the MCP to unlock the live data path.
103
+
104
+ **Is the question bank really open?**
105
+ The Skill, the per-tool dist pipeline, and the install CLI are MIT-licensed in this repo. The question bank itself lives behind the MCP today; the open-data play is on the roadmap.
106
+
107
+ **What about Pi, Gemini CLI, OpenCode, Trae, Rovo Dev, Qoder?**
108
+ On the roadmap. The dist pipeline can target any AI tool with a documented Skill or instructions-file convention. PRs welcome.
109
+
110
+ ## Repo layout
111
+
112
+ ```
113
+ README.md you are here
114
+ LICENSE MIT
115
+ package.json CLI + build wiring, npm metadata
116
+ bin/
117
+ four-leaf-coach.js the `npx four-leaf-coach add` CLI
118
+ scripts/
119
+ build.js generates dist/<tool>/ bundles from the source below
120
+ SKILL.md entry point your AI tool loads (source of truth)
121
+ references/
122
+ mcp-tools.md reference for the eight MCP tools
123
+ upgrade-flow.md paid-tier handling pattern
124
+ commands/ per-command instructions
125
+ kickoff.md
126
+ find-jobs.md
127
+ prep-role.md
128
+ practice.md
129
+ analyze-jd.md
130
+ negotiate-prep.md
131
+ interview-strategy.md
132
+ dist/ generated by `npm run build` (gitignored, not committed)
133
+ ```
134
+
135
+ `SKILL.md` and `references/` are the source of truth. `dist/` is generated output, so edit the source and rerun `npm run build`.
136
+
137
+ ## Roadmap
138
+
139
+ - **Registry submissions** to every Skill aggregator that ships a public registry.
140
+ - **Coverage for more tools** as their Skill conventions stabilize: Pi, Gemini CLI, OpenCode, Trae, Rovo Dev, Qoder.
141
+
142
+ Done so far: per-tool `dist/` bundles generated from a single source (`npm run build`), a flattened single-file variant for GitHub Copilot, and the `npx four-leaf-coach add` one-command installer.
143
+
144
+ ## Contributing
145
+
146
+ PRs welcome that improve a command's coaching, add a new command, or fix a voice issue. Anything that drifts the Skill's positioning away from "coach, not cheat tool" will be declined.
147
+
148
+ ## License
149
+
150
+ MIT. See `LICENSE`.
package/SKILL.md ADDED
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: four-leaf-coach
3
+ description: Job search and interview prep coach. Pulls real postings, role intelligence, and resume scoring from the hosted Four-Leaf MCP. Use when the user wants to find jobs, prep for interviews, practice answers, score a resume against a JD, or work through compensation negotiation. Routes to one of seven guided commands; defaults to `kickoff` when intent is unclear.
4
+ ---
5
+
6
+ # four-leaf-coach
7
+
8
+ You are a job search and interview prep coach. Your job is to walk the user through preparing for real interviews at real companies, using real data instead of generic advice. You have access to the Four-Leaf MCP at `https://four-leaf.ai/api/mcp`, which exposes tools for live job search, role-specific interview intelligence, a curated question bank, and resume scoring.
9
+
10
+ ## Operating principles
11
+
12
+ 1. **Use the MCP. Don't hallucinate.** If a tool can answer the question, call the tool. Don't invent companies, postings, salary bands, or interview formats from training data when the MCP has the real data.
13
+ 2. **Coach, don't cheat.** The user is preparing for a real interview, not gaming one. Help them think clearly, build real skills, and notice their own gaps. If a user asks for live answers they can paste into an active interview, redirect.
14
+ 3. **Push to practice.** Reading about an interview is weaker than practicing one. When the user has enough context, route them to `practice` or to the paid voice mock interview.
15
+ 4. **Be specific.** Reference the user's actual role, company, and seniority. Generic advice is a tell that you didn't use the MCP.
16
+ 5. **Stay short.** Coaching is back-and-forth. Don't dump six paragraphs when one short prompt moves the conversation forward.
17
+
18
+ ## Commands
19
+
20
+ When the user types `/kickoff`, `/find-jobs`, `/prep-role`, `/practice`, `/analyze-jd`, `/negotiate-prep`, or `/interview-strategy`, read the corresponding file in `references/commands/` and follow it. If the user describes intent without using a slash command, infer the right command and either invoke it or ask one clarifying question.
21
+
22
+ If intent is genuinely unclear, run `kickoff`.
23
+
24
+ ## MCP awareness
25
+
26
+ The Skill is useless without the MCP connected. On your first response in a session:
27
+
28
+ 1. Try a cheap tool call (`list_roles` is best because it's fast, free, and read-only).
29
+ 2. If the call succeeds, proceed normally.
30
+ 3. If the call fails because the MCP isn't installed, tell the user once:
31
+ > To get the live data this Skill needs, install the Four-Leaf MCP. Run `claude mcp add --transport http four-leaf https://four-leaf.ai/api/mcp` and authorize in the browser. A free account works.
32
+ Then offer to continue with coaching-only mode (no live data) until they connect.
33
+
34
+ See `references/mcp-tools.md` for the full list of tools and what each returns.
35
+
36
+ ## Upgrade flow
37
+
38
+ Two tools are paid-gated: `start_voice_mock_interview` and (when shipped) `tailor_resume`. When a free user tries to use one, the MCP returns `error: upgrade_required` with a pricing URL. Pass the pricing URL through verbatim and let the user decide. Don't push. The pricing surface explains the three options (3-day free trial, $5 5-Day Pass, $20/mo Pro). Your job is to surface the deep link, not to upsell.
39
+
40
+ See `references/upgrade-flow.md` for the full pattern.
41
+
42
+ ## Voice
43
+
44
+ - Active, specific, short. Contractions on.
45
+ - No em dashes. Use periods or parentheses.
46
+ - No "dive into", "unlock", "leverage", "delve". Plain language.
47
+ - Never name yourself "Claude" in coaching. You're the coach in this Skill, not the assistant.
48
+
49
+ ## What you don't do
50
+
51
+ - You don't make up jobs, companies, salary bands, or interview formats. Use the MCP.
52
+ - You don't claim Four-Leaf has data it doesn't have (e.g., don't promise company-specific interview format intel beyond what `explain_interview_format` returns).
53
+ - You don't write a cover letter or full resume from scratch. The MCP exposes `match_score` (free) for assessment. Full rewriting is paid and happens on Four-Leaf.
54
+ - You don't help the user cheat on a live interview. Hard line.
@@ -0,0 +1,439 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * four-leaf-coach CLI.
4
+ *
5
+ * Installs the four-leaf-coach Skill into a supported AI tool by copying the
6
+ * pre-built bundle from dist/<tool>/ into that tool's config location.
7
+ *
8
+ * Commands:
9
+ * add detect (or take --tool) and install the Skill
10
+ * list show supported tools and whether each is detected here
11
+ *
12
+ * The package ships dist/ pre-built (see prepack), so users don't clone or
13
+ * build. Pure Node stdlib, no dependencies.
14
+ */
15
+
16
+ const fs = require("fs/promises");
17
+ const path = require("path");
18
+ const os = require("os");
19
+ const readline = require("readline/promises");
20
+ const { parseArgs } = require("util");
21
+
22
+ const PKG_ROOT = path.resolve(__dirname, "..");
23
+ const DIST_DIR = path.join(PKG_ROOT, "dist");
24
+ const SKILL_NAME = "four-leaf-coach";
25
+ const MCP_CMD = "claude mcp add --transport http four-leaf https://four-leaf.ai/api/mcp";
26
+
27
+ // Read the version straight from package.json so it never drifts.
28
+ const VERSION = require(path.join(PKG_ROOT, "package.json")).version;
29
+
30
+ const TOOLS = ["claude-code", "cursor", "codex", "github-copilot"];
31
+
32
+ const TOOL_LABELS = {
33
+ "claude-code": "Claude Code",
34
+ cursor: "Cursor",
35
+ codex: "OpenAI Codex CLI",
36
+ "github-copilot": "GitHub Copilot",
37
+ };
38
+
39
+ /* ----------------------------- small helpers ----------------------------- */
40
+
41
+ async function pathExists(p) {
42
+ try {
43
+ await fs.access(p);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /** Recursively copy a directory tree. */
51
+ async function copyDir(src, dest) {
52
+ await fs.mkdir(dest, { recursive: true });
53
+ const entries = await fs.readdir(src, { withFileTypes: true });
54
+ for (const entry of entries) {
55
+ const from = path.join(src, entry.name);
56
+ const to = path.join(dest, entry.name);
57
+ if (entry.isDirectory()) {
58
+ await copyDir(from, to);
59
+ } else if (entry.isFile()) {
60
+ await fs.copyFile(from, to);
61
+ }
62
+ }
63
+ }
64
+
65
+ /** Copy a single file, creating parent directories as needed. */
66
+ async function copyFileTo(src, dest) {
67
+ await fs.mkdir(path.dirname(dest), { recursive: true });
68
+ await fs.copyFile(src, dest);
69
+ }
70
+
71
+ function tilde(p) {
72
+ const home = os.homedir();
73
+ return p.startsWith(home) ? p.replace(home, "~") : p;
74
+ }
75
+
76
+ function bail(message) {
77
+ console.error(`Error: ${message}`);
78
+ process.exit(1);
79
+ }
80
+
81
+ /* ------------------------------ detection -------------------------------- */
82
+
83
+ /**
84
+ * Inspect cwd and $HOME for signals of each tool. Returns an array of
85
+ * { tool, scope, reason } candidates, most specific first.
86
+ */
87
+ async function detectTools(cwd, home) {
88
+ const candidates = [];
89
+
90
+ if (await pathExists(path.join(cwd, ".cursor"))) {
91
+ candidates.push({ tool: "cursor", scope: "project", reason: ".cursor/ in this directory" });
92
+ }
93
+ if (await pathExists(path.join(cwd, ".claude"))) {
94
+ candidates.push({ tool: "claude-code", scope: "project", reason: ".claude/ in this directory" });
95
+ }
96
+ if (await pathExists(path.join(home, ".claude", "skills"))) {
97
+ candidates.push({ tool: "claude-code", scope: "global", reason: "~/.claude/skills/ exists" });
98
+ }
99
+ if (await pathExists(path.join(cwd, "AGENTS.md"))) {
100
+ candidates.push({ tool: "codex", scope: "project", reason: "AGENTS.md in this directory" });
101
+ }
102
+ if (await pathExists(path.join(cwd, ".github"))) {
103
+ candidates.push({ tool: "github-copilot", scope: "project", reason: ".github/ in this directory" });
104
+ }
105
+
106
+ return candidates;
107
+ }
108
+
109
+ /* ------------------------------- prompts --------------------------------- */
110
+
111
+ async function ask(question) {
112
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
113
+ try {
114
+ const answer = await rl.question(question);
115
+ return answer.trim();
116
+ } finally {
117
+ rl.close();
118
+ }
119
+ }
120
+
121
+ async function confirm(question) {
122
+ const answer = (await ask(`${question} [y/N] `)).toLowerCase();
123
+ return answer === "y" || answer === "yes";
124
+ }
125
+
126
+ /** Present a numbered menu and return the chosen item. */
127
+ async function choose(label, items, renderItem) {
128
+ console.log(`\n${label}`);
129
+ items.forEach((item, i) => {
130
+ console.log(` ${i + 1}) ${renderItem(item)}`);
131
+ });
132
+ while (true) {
133
+ const raw = await ask(`Choose 1-${items.length}: `);
134
+ const n = Number(raw);
135
+ if (Number.isInteger(n) && n >= 1 && n <= items.length) {
136
+ return items[n - 1];
137
+ }
138
+ console.log("Enter a number from the list.");
139
+ }
140
+ }
141
+
142
+ /* ----------------------------- resolve target ---------------------------- */
143
+
144
+ /**
145
+ * Given the chosen tool, scope, and base directory, return the install plan:
146
+ * { tool, scope, source, kind, target, existing }.
147
+ * kind: "dir" (copy a tree) or "tree+entry" (codex) or "file" (copilot)
148
+ */
149
+ async function buildPlan(tool, scope, base) {
150
+ switch (tool) {
151
+ case "claude-code": {
152
+ const source = path.join(DIST_DIR, "claude-code", ".claude", "skills", SKILL_NAME);
153
+ const target = path.join(base, ".claude", "skills", SKILL_NAME);
154
+ return { tool, scope, kind: "skill-dir", source, target, existing: await pathExists(target) };
155
+ }
156
+ case "cursor": {
157
+ const source = path.join(DIST_DIR, "cursor", ".cursor", "skills", SKILL_NAME);
158
+ const target = path.join(base, ".cursor", "skills", SKILL_NAME);
159
+ return { tool, scope, kind: "skill-dir", source, target, existing: await pathExists(target) };
160
+ }
161
+ case "codex": {
162
+ const source = path.join(DIST_DIR, "codex");
163
+ const agents = path.join(base, "AGENTS.md");
164
+ return {
165
+ tool,
166
+ scope,
167
+ kind: "codex",
168
+ source,
169
+ target: base,
170
+ agentsPath: agents,
171
+ existing: await pathExists(agents),
172
+ };
173
+ }
174
+ case "github-copilot": {
175
+ const source = path.join(DIST_DIR, "github", ".github", "copilot-instructions.md");
176
+ const target = path.join(base, ".github", "copilot-instructions.md");
177
+ return { tool, scope, kind: "file", source, target, existing: await pathExists(target) };
178
+ }
179
+ default:
180
+ bail(`unknown tool "${tool}". Supported: ${TOOLS.join(", ")}`);
181
+ }
182
+ }
183
+
184
+ /* -------------------------------- install -------------------------------- */
185
+
186
+ async function runInstall(plan, { dryRun }) {
187
+ const tag = dryRun ? "[dry-run] " : "";
188
+
189
+ switch (plan.kind) {
190
+ case "skill-dir": {
191
+ console.log(`${tag}Write Skill to ${tilde(plan.target)}/`);
192
+ if (!dryRun) {
193
+ await fs.rm(plan.target, { recursive: true, force: true });
194
+ await copyDir(plan.source, plan.target);
195
+ }
196
+ break;
197
+ }
198
+ case "codex": {
199
+ console.log(`${tag}Write ${tilde(plan.agentsPath)}`);
200
+ console.log(`${tag}Write ${tilde(path.join(plan.target, "references"))}/`);
201
+ if (!dryRun) {
202
+ await copyFileTo(path.join(plan.source, "AGENTS.md"), plan.agentsPath);
203
+ await copyDir(path.join(plan.source, "references"), path.join(plan.target, "references"));
204
+ }
205
+ break;
206
+ }
207
+ case "file": {
208
+ console.log(`${tag}Write ${tilde(plan.target)}`);
209
+ if (!dryRun) {
210
+ await copyFileTo(plan.source, plan.target);
211
+ }
212
+ break;
213
+ }
214
+ default:
215
+ bail(`internal error: unknown plan kind "${plan.kind}"`);
216
+ }
217
+ }
218
+
219
+ function printNextSteps(plan) {
220
+ const scopeNote = plan.tool === "claude-code" ? ` (${plan.scope})` : "";
221
+ console.log(`\nInstalled ${SKILL_NAME} for ${TOOL_LABELS[plan.tool]}${scopeNote}.`);
222
+
223
+ console.log("\nNext step: install the Four-Leaf MCP for live data.");
224
+ console.log(` ${MCP_CMD}`);
225
+
226
+ if (plan.tool === "claude-code") {
227
+ console.log("\nThen type /kickoff in Claude to start.");
228
+ } else if (plan.tool === "cursor") {
229
+ console.log("\nEnable Agent Skills (Settings, Rules, Agent Skills on the Nightly channel), then type /kickoff.");
230
+ } else if (plan.tool === "codex") {
231
+ console.log("\nRun Codex from this directory, then type kickoff to start.");
232
+ } else if (plan.tool === "github-copilot") {
233
+ console.log("\nCopilot picks up .github/copilot-instructions.md automatically. Ask it to run kickoff.");
234
+ }
235
+ }
236
+
237
+ /* --------------------------------- add ----------------------------------- */
238
+
239
+ async function cmdAdd(opts) {
240
+ if (!(await pathExists(DIST_DIR))) {
241
+ bail("dist/ not found. If you're running from a clone, run `npm run build` first.");
242
+ }
243
+
244
+ const cwd = process.cwd();
245
+ const home = os.homedir();
246
+
247
+ if (opts.tool && !TOOLS.includes(opts.tool)) {
248
+ bail(`unknown --tool "${opts.tool}". Supported: ${TOOLS.join(", ")}`);
249
+ }
250
+ if (opts.scope && !["project", "global"].includes(opts.scope)) {
251
+ bail(`unknown --scope "${opts.scope}". Use project or global.`);
252
+ }
253
+
254
+ // 1. Decide the tool.
255
+ let tool = opts.tool;
256
+ let scope = opts.scope;
257
+
258
+ if (!tool) {
259
+ const candidates = await detectTools(cwd, home);
260
+ if (candidates.length === 1) {
261
+ tool = candidates[0].tool;
262
+ scope = scope || candidates[0].scope;
263
+ console.log(`Detected ${TOOL_LABELS[tool]} (${candidates[0].reason}).`);
264
+ } else if (candidates.length > 1) {
265
+ const picked = await choose(
266
+ "Found more than one tool. Which one?",
267
+ candidates,
268
+ (c) => `${TOOL_LABELS[c.tool]} (${c.reason})`
269
+ );
270
+ tool = picked.tool;
271
+ scope = scope || picked.scope;
272
+ } else {
273
+ const picked = await choose(
274
+ "No tool detected here. Which one are you installing for?",
275
+ TOOLS.map((t) => ({ tool: t })),
276
+ (c) => TOOL_LABELS[c.tool] + (c.tool === "claude-code" ? " (default)" : "")
277
+ );
278
+ tool = picked.tool;
279
+ }
280
+ }
281
+
282
+ // 2. Decide scope + base directory.
283
+ let base;
284
+ if (opts.dir) {
285
+ // Explicit base directory. The tool's standard subpath is appended.
286
+ base = path.resolve(opts.dir);
287
+ scope = scope || (tool === "claude-code" ? "global" : "project");
288
+ } else if (tool === "claude-code") {
289
+ if (!scope) {
290
+ const hasProject = await pathExists(path.join(cwd, ".claude"));
291
+ const hasGlobal = await pathExists(path.join(home, ".claude"));
292
+ if (hasProject && hasGlobal) {
293
+ const picked = await choose(
294
+ "Install Claude Code Skill where?",
295
+ [
296
+ { scope: "global", where: tilde(path.join(home, ".claude", "skills")) },
297
+ { scope: "project", where: path.join(cwd, ".claude", "skills") },
298
+ ],
299
+ (c) => `${c.scope} (${c.where})`
300
+ );
301
+ scope = picked.scope;
302
+ } else if (hasProject) {
303
+ scope = "project";
304
+ } else {
305
+ scope = "global";
306
+ }
307
+ }
308
+ base = scope === "global" ? home : cwd;
309
+ } else {
310
+ scope = scope || "project";
311
+ base = cwd;
312
+ }
313
+
314
+ // 3. Build the plan and handle overwrites.
315
+ const plan = await buildPlan(tool, scope, base);
316
+
317
+ if (plan.existing && !opts.force && !opts.yes && !opts.dryRun) {
318
+ const what =
319
+ plan.kind === "skill-dir"
320
+ ? `${tilde(plan.target)}/`
321
+ : plan.kind === "codex"
322
+ ? tilde(plan.agentsPath)
323
+ : tilde(plan.target);
324
+ const ok = await confirm(`${what} already exists. Replace?`);
325
+ if (!ok) {
326
+ console.log("Cancelled. Nothing was written.");
327
+ return;
328
+ }
329
+ } else if (plan.existing && opts.dryRun) {
330
+ console.log(`[dry-run] ${tilde(plan.target)} already exists and would be replaced.`);
331
+ }
332
+
333
+ // 4. Install.
334
+ await runInstall(plan, { dryRun: opts.dryRun });
335
+ printNextSteps(plan);
336
+ }
337
+
338
+ /* --------------------------------- list ---------------------------------- */
339
+
340
+ async function cmdList() {
341
+ const cwd = process.cwd();
342
+ const home = os.homedir();
343
+ const detected = await detectTools(cwd, home);
344
+ const byTool = new Set(detected.map((c) => c.tool));
345
+
346
+ console.log("Supported tools:\n");
347
+ for (const tool of TOOLS) {
348
+ const hits = detected.filter((c) => c.tool === tool);
349
+ const status = byTool.has(tool)
350
+ ? `detected (${hits.map((h) => h.reason).join(", ")})`
351
+ : "not detected here";
352
+ console.log(` ${TOOL_LABELS[tool].padEnd(18)} ${status}`);
353
+ }
354
+ console.log(`\nInstall with: four-leaf-coach add [--tool <name>]`);
355
+ }
356
+
357
+ /* --------------------------------- usage --------------------------------- */
358
+
359
+ function printUsage() {
360
+ console.log(`four-leaf-coach ${VERSION}
361
+
362
+ Install the four-leaf-coach Skill into your AI tool.
363
+
364
+ Usage:
365
+ four-leaf-coach add [options] Detect your tool and install the Skill
366
+ four-leaf-coach list Show supported tools and what's detected here
367
+
368
+ Options for add:
369
+ --tool <name> claude-code | cursor | codex | github-copilot
370
+ --scope <s> project | global (Claude Code only; default global)
371
+ --dir <path> Install under this base directory instead of detecting
372
+ --yes, -y Skip confirmation prompts
373
+ --force Overwrite existing files without asking
374
+ --dry-run Print what would happen, write nothing
375
+
376
+ Other:
377
+ --help, -h Show this help
378
+ --version Print the version
379
+
380
+ After installing, add the Four-Leaf MCP for live data:
381
+ ${MCP_CMD}`);
382
+ }
383
+
384
+ /* --------------------------------- main ---------------------------------- */
385
+
386
+ async function main() {
387
+ const argv = process.argv.slice(2);
388
+
389
+ const { values, positionals } = parseArgs({
390
+ args: argv,
391
+ allowPositionals: true,
392
+ options: {
393
+ tool: { type: "string" },
394
+ scope: { type: "string" },
395
+ dir: { type: "string" },
396
+ yes: { type: "boolean", short: "y", default: false },
397
+ force: { type: "boolean", default: false },
398
+ "dry-run": { type: "boolean", default: false },
399
+ help: { type: "boolean", short: "h", default: false },
400
+ version: { type: "boolean", default: false },
401
+ },
402
+ });
403
+
404
+ if (values.version) {
405
+ console.log(VERSION);
406
+ return;
407
+ }
408
+
409
+ const command = positionals[0];
410
+
411
+ if (values.help || command === "help" || !command) {
412
+ printUsage();
413
+ // No command at all is a usage error; explicit --help/help is success.
414
+ if (!command && !values.help) process.exitCode = 1;
415
+ return;
416
+ }
417
+
418
+ switch (command) {
419
+ case "add":
420
+ await cmdAdd({
421
+ tool: values.tool,
422
+ scope: values.scope,
423
+ dir: values.dir,
424
+ yes: values.yes,
425
+ force: values.force,
426
+ dryRun: values["dry-run"],
427
+ });
428
+ break;
429
+ case "list":
430
+ await cmdList();
431
+ break;
432
+ default:
433
+ console.error(`Unknown command "${command}".\n`);
434
+ printUsage();
435
+ process.exitCode = 1;
436
+ }
437
+ }
438
+
439
+ main().catch((err) => bail(err.stack || String(err)));