Skip to content

Commit c02a4af

Browse files
committed
ep-cli: add agent/claude install targets with managed section upsert
1 parent ff53e10 commit c02a4af

4 files changed

Lines changed: 144 additions & 47 deletions

File tree

packages/ep-cli/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,28 +272,32 @@ Subcommands:
272272

273273
`install remove` and `install diff` exist in code but are intentionally not exposed in the public command surface at this time.
274274

275-
### `ep install add --tool text [--skill-level text] [--use-case text] [-i|--interactive]`
275+
### `ep install add [--tool text] [--skill-level text] [--use-case text] [-i|--interactive]`
276276

277277
Installs rule content into local tool configuration files.
278278

279279
Supported tool values:
280280

281-
- `agents`
281+
- `agent` (default)
282+
- `claude`
282283
- `cursor`
283284
- `vscode`
284285
- `windsurf`
285286

286287
Target files:
287288

288-
- `agents` -> rule content is written to `docs/Effect-Patterns-Rules.md`; `AGENTS.md` (in cwd) is updated with a managed block that points to that file.
289+
- `agent` -> `AGENTS.md` (in cwd), managed Effect section is upserted in place
290+
- `claude` -> `CLAUDE.md` (in cwd), managed Effect section is upserted in place
289291
- `cursor` -> `.cursor/rules.md`
290292
- `vscode` -> `.vscode/rules.md`
291293
- `windsurf` -> `.windsurf/rules.md`
292294

293295
Examples:
294296

295297
```bash
296-
ep install add --tool agents
298+
ep install add
299+
ep install add --tool agent
300+
ep install add --tool claude
297301
ep install add --tool cursor --skill-level intermediate
298302
ep install add --tool vscode --use-case building-apis
299303
ep install add --tool windsurf -i
@@ -304,7 +308,8 @@ Notes:
304308
- `--interactive` opens multi-select prompt.
305309
- `--skill-level` maps to pattern difficulty filter.
306310
- `--use-case` filters results client-side by use case tag.
307-
- For `agents`, rule content is written to `docs/Effect-Patterns-Rules.md`; `AGENTS.md` is then updated with a managed block (`<!-- EP_RULES_START -->` / `<!-- EP_RULES_END -->`) that references that file. An existing managed block in `AGENTS.md` is replaced in place.
311+
- `agent` and `claude` use managed sections in `AGENTS.md` / `CLAUDE.md` and update only those sections when rerun.
312+
- Legacy alias `agents` is accepted and maps to `agent`.
308313
- State is recorded in installed-rules state JSON file.
309314

310315
Unsupported tool behavior:
@@ -330,7 +335,7 @@ ep install list --installed --json
330335
JSON shapes:
331336

332337
```json
333-
{ "tools": ["agents", "cursor", "vscode", "windsurf"] }
338+
{ "tools": ["agent", "claude", "cursor", "vscode", "windsurf"] }
334339
```
335340

336341
```json

packages/ep-cli/src/__tests__/cli-contract.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ describe("ep-cli stream and machine-mode contracts", () => {
6767
const parsed = JSON.parse(result.stdout);
6868
expect(Array.isArray(parsed.tools)).toBe(true);
6969
expect(parsed.tools).toContain("cursor");
70+
expect(parsed.tools).toContain("agent");
71+
expect(parsed.tools).toContain("claude");
7072
});
7173

7274
it("keeps unsupported-tool diagnostics on stderr", () => {

packages/ep-cli/src/commands/__tests__/install-commands.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ describe("install commands", () => {
3434
const exit = await run(["install", "list"]);
3535
expect(Exit.isSuccess(exit)).toBe(true);
3636
const output = capture.logs.join("\n");
37-
expect(output).toContain("agents");
37+
expect(output).toContain("agent");
38+
expect(output).toContain("claude");
3839
expect(output).toContain("cursor");
3940
expect(output).toContain("vscode");
4041
expect(output).toContain("windsurf");
@@ -47,7 +48,8 @@ describe("install commands", () => {
4748
const parsed = JSON.parse(jsonLines);
4849
expect(Array.isArray(parsed.tools)).toBe(true);
4950
expect(parsed.tools).toContain("cursor");
50-
expect(parsed.tools).toContain("agents");
51+
expect(parsed.tools).toContain("agent");
52+
expect(parsed.tools).toContain("claude");
5153
});
5254

5355
it("outputs JSON for --installed --json with no rules", async () => {

packages/ep-cli/src/commands/install-commands.ts

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,109 @@ import { Install, InstalledRule } from "../services/install/index.js";
1111
import { colorize } from "../utils.js";
1212
import { UnsupportedToolError } from "../errors.js";
1313

14+
type InstallTool = "agent" | "claude" | "cursor" | "vscode" | "windsurf";
15+
16+
const EFFECT_AGENT_START = "<!-- EFFECT_SKILLS_AGENT_START -->";
17+
const EFFECT_AGENT_END = "<!-- EFFECT_SKILLS_AGENT_END -->";
18+
const EFFECT_CLAUDE_START = "<!-- EFFECT_SKILLS_CLAUDE_START -->";
19+
const EFFECT_CLAUDE_END = "<!-- EFFECT_SKILLS_CLAUDE_END -->";
20+
21+
const normalizeTool = (tool: string): InstallTool | null => {
22+
if (tool === "agents") return "agent";
23+
if (tool === "agent" || tool === "claude" || tool === "cursor" || tool === "vscode" || tool === "windsurf") {
24+
return tool;
25+
}
26+
return null;
27+
};
28+
29+
const installTargetByTool: Record<InstallTool, string> = {
30+
agent: "AGENTS.md",
31+
claude: "CLAUDE.md",
32+
cursor: ".cursor/rules.md",
33+
vscode: ".vscode/rules.md",
34+
windsurf: ".windsurf/rules.md",
35+
};
36+
37+
const markerByTool: Partial<Record<InstallTool, { start: string; end: string }>> = {
38+
agent: { start: EFFECT_AGENT_START, end: EFFECT_AGENT_END },
39+
claude: { start: EFFECT_CLAUDE_START, end: EFFECT_CLAUDE_END },
40+
};
41+
42+
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43+
44+
const upsertManagedSection = (
45+
current: string,
46+
startMarker: string,
47+
endMarker: string,
48+
sectionBody: string
49+
): string => {
50+
const managedSection = `${startMarker}\n${sectionBody}\n${endMarker}\n`;
51+
const replacePattern = new RegExp(
52+
`${escapeRegex(startMarker)}[\\s\\S]*?${escapeRegex(endMarker)}\\n?`,
53+
"m"
54+
);
55+
56+
if (current.includes(startMarker) && current.includes(endMarker)) {
57+
return current.replace(replacePattern, managedSection);
58+
}
59+
60+
if (current.trim().length === 0) {
61+
return managedSection;
62+
}
63+
64+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
65+
return `${current}${separator}${managedSection}`;
66+
};
67+
68+
const buildAgentSection = (
69+
rules: ReadonlyArray<Pick<InstalledRule, "id" | "title" | "skillLevel" | "useCase" | "description">>,
70+
generatedAtIso: string
71+
): string =>
72+
[
73+
"## Effect Skills",
74+
"",
75+
`Generated by ep on ${generatedAtIso}.`,
76+
"",
77+
...rules.flatMap((rule) => [
78+
`### ${rule.title}`,
79+
`- ID: ${rule.id}`,
80+
`- Skill Level: ${rule.skillLevel ?? "general"}`,
81+
`- Use Cases: ${(rule.useCase ?? []).join(", ") || "none"}`,
82+
"",
83+
rule.description,
84+
"",
85+
]),
86+
].join("\n");
87+
88+
const buildClaudeSection = (
89+
rules: ReadonlyArray<Pick<InstalledRule, "title" | "skillLevel" | "useCase" | "description">>,
90+
generatedAtIso: string
91+
): string =>
92+
[
93+
"## Coding Standards (Effect Patterns)",
94+
"",
95+
`Updated: ${generatedAtIso}`,
96+
"",
97+
"### Guiding Principles",
98+
"",
99+
...rules.map((rule) => {
100+
const level = rule.skillLevel ?? "general";
101+
const useCases = (rule.useCase ?? []).slice(0, 3).join(", ");
102+
const useCaseSuffix = useCases.length > 0 ? ` [${useCases}]` : "";
103+
return `- **${rule.title}** (${level})${useCaseSuffix}: ${rule.description}`;
104+
}),
105+
"",
106+
"Use these as defaults when making implementation decisions in this repository.",
107+
].join("\n");
108+
14109
/**
15110
* install:add - Add rules to AI tool configuration
16111
*/
17112
export const installAddCommand = Command.make("add", {
18113
options: {
19114
tool: Options.text("tool").pipe(
20-
Options.withDescription("The AI tool to install rules for (cursor, agents, vscode, windsurf)")
115+
Options.withDescription("Target tool format (agent, claude, cursor, vscode, windsurf)"),
116+
Options.withDefault("agent")
21117
),
22118
skillLevel: Options.optional(
23119
Options.text("skill-level").pipe(
@@ -42,25 +138,20 @@ export const installAddCommand = Command.make("add", {
42138
const { loadInstalledRules, saveInstalledRules, searchRules } = yield* Install;
43139
const fs = yield* FileSystem.FileSystem;
44140

45-
const installTargetByTool: Record<string, string> = {
46-
agents: "AGENTS.md",
47-
cursor: ".cursor/rules.md",
48-
vscode: ".vscode/rules.md",
49-
windsurf: ".windsurf/rules.md",
50-
};
51-
52-
const targetPath = installTargetByTool[options.tool];
53-
if (!targetPath) {
141+
const normalizedTool = normalizeTool(options.tool);
142+
if (!normalizedTool) {
143+
const supportedTools = ["agent", "claude", "cursor", "vscode", "windsurf"];
54144
yield* Display.showError(
55-
`Tool '${options.tool}' is not supported by local file injection yet. Supported: ${Object.keys(installTargetByTool).join(", ")}`
145+
`Tool '${options.tool}' is not supported by local file injection yet. Supported: ${supportedTools.join(", ")}`
56146
);
57147
return yield* Effect.fail(new UnsupportedToolError({
58148
tool: options.tool,
59-
supported: Object.keys(installTargetByTool),
149+
supported: supportedTools,
60150
}));
61151
}
152+
const targetPath = installTargetByTool[normalizedTool];
62153

63-
yield* Console.log(colorize("\nInstalling rules for " + options.tool + "...\n", "CYAN"));
154+
yield* Console.log(colorize("\nInstalling rules for " + normalizedTool + "...\n", "CYAN"));
64155

65156
let rulesToInstall = yield* searchRules({
66157
skillLevel: Option.getOrUndefined(options.skillLevel),
@@ -85,10 +176,11 @@ export const installAddCommand = Command.make("add", {
85176
return;
86177
}
87178

179+
const generatedAt = new Date().toISOString();
88180
const rulesMarkdown = [
89181
"# Effect Patterns Rules",
90182
"",
91-
`Generated by ep on ${new Date().toISOString()}.`,
183+
`Generated by ep on ${generatedAt}.`,
92184
"Source: Effect Patterns Database",
93185
"",
94186
...rulesToInstall.flatMap((rule) => [
@@ -105,27 +197,24 @@ export const installAddCommand = Command.make("add", {
105197
]),
106198
].join("\n");
107199

108-
if (options.tool === "agents") {
109-
const startMarker = "<!-- EP_RULES_START -->";
110-
const endMarker = "<!-- EP_RULES_END -->";
111-
const canonicalPath = path.join(process.cwd(), "docs", "Effect-Patterns-Rules.md");
112-
const docsDir = path.dirname(canonicalPath);
113-
114-
yield* fs.makeDirectory(docsDir, { recursive: true });
115-
yield* fs.writeFileString(canonicalPath, rulesMarkdown);
116-
117-
const pointerLine = "For Effect Patterns rules, see [docs/Effect-Patterns-Rules.md](docs/Effect-Patterns-Rules.md).";
118-
const managedSection = `${startMarker}\n\n${pointerLine}\n\n${endMarker}\n`;
200+
if (normalizedTool === "agent" || normalizedTool === "claude") {
201+
const markers = markerByTool[normalizedTool];
202+
if (!markers) {
203+
return yield* Effect.fail(new Error(`Missing marker configuration for tool: ${normalizedTool}`));
204+
}
119205

206+
const managedBody =
207+
normalizedTool === "agent"
208+
? buildAgentSection(rulesToInstall, generatedAt)
209+
: buildClaudeSection(rulesToInstall, generatedAt);
120210
const exists = yield* fs.exists(targetPath);
121211
const current = exists ? yield* fs.readFileString(targetPath) : "";
122-
const nextContent =
123-
current.includes(startMarker) && current.includes(endMarker)
124-
? current.replace(
125-
/<!-- EP_RULES_START -->[\s\S]*<!-- EP_RULES_END -->\n?/m,
126-
managedSection
127-
)
128-
: `${current}${current.endsWith("\n") ? "" : "\n"}\n${managedSection}`;
212+
const nextContent = upsertManagedSection(
213+
current,
214+
markers.start,
215+
markers.end,
216+
managedBody
217+
);
129218

130219
yield* fs.writeFileString(targetPath, nextContent);
131220
} else {
@@ -138,7 +227,7 @@ export const installAddCommand = Command.make("add", {
138227
const newRules: InstalledRule[] = rulesToInstall.map((r) => ({
139228
...r,
140229
installedAt: new Date().toISOString(),
141-
tool: options.tool,
230+
tool: normalizedTool,
142231
version: "1.0.0",
143232
}));
144233
const merged = [
@@ -147,10 +236,8 @@ export const installAddCommand = Command.make("add", {
147236
];
148237
yield* saveInstalledRules(merged);
149238

150-
if (options.tool === "agents") {
151-
yield* Display.showSuccess(
152-
`Installed ${rulesToInstall.length} rule(s) to docs/Effect-Patterns-Rules.md and updated ${targetPath}`
153-
);
239+
if (normalizedTool === "agent" || normalizedTool === "claude") {
240+
yield* Console.log(`✅ Installed Effect Skills to ${targetPath}`);
154241
} else {
155242
yield* Display.showSuccess(`Installed ${rulesToInstall.length} rule(s) to ${targetPath}`);
156243
}
@@ -186,7 +273,8 @@ const displayInstalledRules = (
186273
const displaySupportedTools = (): Effect.Effect<void, unknown> =>
187274
Effect.gen(function* () {
188275
yield* Console.log(colorize("\n📋 Supported AI Tools\n", "BRIGHT"));
189-
yield* Console.log(" • agents");
276+
yield* Console.log(" • agent");
277+
yield* Console.log(" • claude");
190278
yield* Console.log(" • cursor");
191279
yield* Console.log(" • vscode");
192280
yield* Console.log(" • windsurf");
@@ -211,7 +299,7 @@ export const installListCommand = Command.make("list", {
211299
Command.withHandler(({ options }) =>
212300
Effect.gen(function* () {
213301
const { loadInstalledRules } = yield* Install;
214-
const supportedTools = ["agents", "cursor", "vscode", "windsurf"] as const;
302+
const supportedTools = ["agent", "claude", "cursor", "vscode", "windsurf"] as const;
215303

216304
if (options.json) {
217305
if (options.installed) {

0 commit comments

Comments
 (0)