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.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/SKILL.md +54 -0
- package/bin/four-leaf-coach.js +439 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/analyze-jd.md +44 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/find-jobs.md +41 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/interview-strategy.md +45 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/kickoff.md +41 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/negotiate-prep.md +80 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/practice.md +49 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/commands/prep-role.md +43 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/mcp-tools.md +57 -0
- package/dist/claude-code/.claude/skills/four-leaf-coach/references/upgrade-flow.md +38 -0
- package/dist/codex/AGENTS.md +54 -0
- package/dist/codex/references/commands/analyze-jd.md +44 -0
- package/dist/codex/references/commands/find-jobs.md +41 -0
- package/dist/codex/references/commands/interview-strategy.md +45 -0
- package/dist/codex/references/commands/kickoff.md +41 -0
- package/dist/codex/references/commands/negotiate-prep.md +80 -0
- package/dist/codex/references/commands/practice.md +49 -0
- package/dist/codex/references/commands/prep-role.md +43 -0
- package/dist/codex/references/mcp-tools.md +57 -0
- package/dist/codex/references/upgrade-flow.md +38 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/SKILL.md +54 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/analyze-jd.md +44 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/find-jobs.md +41 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/interview-strategy.md +45 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/kickoff.md +41 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/negotiate-prep.md +80 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/practice.md +49 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/commands/prep-role.md +43 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/mcp-tools.md +57 -0
- package/dist/cursor/.cursor/skills/four-leaf-coach/references/upgrade-flow.md +38 -0
- package/dist/github/.github/copilot-instructions.md +516 -0
- package/package.json +46 -0
- package/references/commands/analyze-jd.md +44 -0
- package/references/commands/find-jobs.md +41 -0
- package/references/commands/interview-strategy.md +45 -0
- package/references/commands/kickoff.md +41 -0
- package/references/commands/negotiate-prep.md +80 -0
- package/references/commands/practice.md +49 -0
- package/references/commands/prep-role.md +43 -0
- package/references/mcp-tools.md +57 -0
- package/references/upgrade-flow.md +38 -0
- 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)));
|