@eric0117/agentforge 0.1.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/add-agent.js +145 -0
  4. package/dist/add-skill.js +185 -0
  5. package/dist/agent-prompt.js +211 -0
  6. package/dist/agentforge-config.js +106 -0
  7. package/dist/agents/claude.js +46 -0
  8. package/dist/agents/codex.js +67 -0
  9. package/dist/agents/cursor.js +54 -0
  10. package/dist/agents/index.js +15 -0
  11. package/dist/agents/io.js +252 -0
  12. package/dist/agents/types.js +1 -0
  13. package/dist/cli.js +374 -0
  14. package/dist/confirm.js +20 -0
  15. package/dist/doctor.js +223 -0
  16. package/dist/enter.js +85 -0
  17. package/dist/init.js +272 -0
  18. package/dist/lang-prompt.js +88 -0
  19. package/dist/list-skills.js +120 -0
  20. package/dist/logo.js +181 -0
  21. package/dist/path-prompt.js +148 -0
  22. package/dist/remove-agent.js +63 -0
  23. package/dist/remove-skill.js +88 -0
  24. package/dist/rename.js +222 -0
  25. package/dist/skill-prompt.js +199 -0
  26. package/dist/skills-data.js +727 -0
  27. package/dist/sync-skills.js +59 -0
  28. package/dist/templates/CLAUDE.md.tpl +141 -0
  29. package/dist/templates/context-handoff.SKILL.md.tpl +222 -0
  30. package/dist/templates/cross-repo-impact.SKILL.md.tpl +241 -0
  31. package/dist/templates/feature-retro.SKILL.md.tpl +312 -0
  32. package/dist/templates/feature-start.SKILL.md.tpl +631 -0
  33. package/dist/templates/history.SKILL.md.tpl +165 -0
  34. package/dist/templates/incident-context.SKILL.md.tpl +260 -0
  35. package/dist/templates/pr-create.SKILL.md.tpl +403 -0
  36. package/dist/templates/pr-review-analyze.SKILL.md.tpl +303 -0
  37. package/dist/templates/pre-deploy-check.SKILL.md.tpl +350 -0
  38. package/dist/templates/project-router.SKILL.md.tpl +55 -0
  39. package/dist/templates/release-coordinate.SKILL.md.tpl +209 -0
  40. package/package.json +54 -0
@@ -0,0 +1,211 @@
1
+ import { AGENTS } from "./agents/index.js";
2
+ const CYAN = "\x1b[36m";
3
+ const GREEN = "\x1b[32m";
4
+ const DIM = "\x1b[2m";
5
+ const BOLD = "\x1b[1m";
6
+ const YELLOW = "\x1b[33m";
7
+ const STRIKE = "\x1b[9m";
8
+ const NOSTRIKE = "\x1b[29m";
9
+ const RESET = "\x1b[0m";
10
+ function wrap(text, width) {
11
+ const out = [];
12
+ for (const paragraph of text.split("\n")) {
13
+ if (paragraph === "") {
14
+ out.push("");
15
+ continue;
16
+ }
17
+ let line = "";
18
+ for (const word of paragraph.split(/\s+/)) {
19
+ if (line.length === 0) {
20
+ line = word;
21
+ }
22
+ else if (line.length + 1 + word.length <= width) {
23
+ line += " " + word;
24
+ }
25
+ else {
26
+ out.push(line);
27
+ line = word;
28
+ }
29
+ }
30
+ if (line)
31
+ out.push(line);
32
+ }
33
+ return out;
34
+ }
35
+ async function readKey() {
36
+ return new Promise((resolve) => {
37
+ process.stdin.once("data", (chunk) => {
38
+ resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
39
+ });
40
+ });
41
+ }
42
+ export async function pickAgents(lang, options = {}) {
43
+ const disabled = options.disabled ?? new Set();
44
+ const headerLabel = options.headerLabel ?? "Agents to set up";
45
+ if (!process.stdin.isTTY) {
46
+ // safe non-TTY default: pick claude if it's not already installed,
47
+ // otherwise nothing (caller decides what to do with an empty pick)
48
+ return disabled.has("claude") ? [] : ["claude"];
49
+ }
50
+ let cursor = 0;
51
+ // default selection: claude (if not already installed)
52
+ const selected = new Set(disabled.has("claude") ? [] : ["claude"]);
53
+ let view = "list";
54
+ let lastLineCount = 0;
55
+ let errorMessage = ""; // shown when Enter is pressed with nothing selected (when required)
56
+ const HEADER_LIST = `${CYAN}?${RESET} ${BOLD}${headerLabel}${RESET} ${DIM}↑↓ move · space toggle · → details · enter confirm${RESET}`;
57
+ const headerDetails = (label, idx, total) => `${CYAN}?${RESET} ${BOLD}${label}${RESET} ${DIM}(${idx}/${total}) ↑↓ next · ← back${RESET}`;
58
+ const renderList = () => {
59
+ const lines = [HEADER_LIST, ""];
60
+ for (let i = 0; i < AGENTS.length; i++) {
61
+ const item = AGENTS[i];
62
+ const isLocked = disabled.has(item.id);
63
+ const isSel = selected.has(item.id);
64
+ const isCur = i === cursor;
65
+ const prefix = isCur ? `${CYAN}❯${RESET}` : " ";
66
+ if (isLocked) {
67
+ const marker = `${DIM}🔒${RESET}`;
68
+ const title = `${DIM}${STRIKE}${item.label}${NOSTRIKE}${RESET}`;
69
+ const tag = `${DIM}(already installed)${RESET}`;
70
+ lines.push(`${prefix} ${marker} ${title} ${tag}`);
71
+ continue;
72
+ }
73
+ const marker = isSel ? `${GREEN}◉${RESET}` : `${DIM}◯${RESET}`;
74
+ const title = isCur ? `${CYAN}${item.label}${RESET}` : item.label;
75
+ lines.push(`${prefix} ${marker} ${title}`);
76
+ }
77
+ if (errorMessage) {
78
+ lines.push("");
79
+ lines.push(` ${YELLOW}⚠${RESET} ${errorMessage}`);
80
+ }
81
+ return lines.join("\n") + "\n";
82
+ };
83
+ const renderDetails = () => {
84
+ const item = AGENTS[cursor];
85
+ const isSel = selected.has(item.id);
86
+ const status = isSel
87
+ ? `${GREEN}◉ selected${RESET}`
88
+ : `${DIM}◯ not selected${RESET}`;
89
+ const width = Math.min((process.stdout.columns || 80) - 4, 78);
90
+ const bar = `${DIM}${"─".repeat(width)}${RESET}`;
91
+ const lines = [
92
+ headerDetails(item.label, cursor + 1, AGENTS.length),
93
+ ` ${bar}`,
94
+ "",
95
+ ];
96
+ for (const w of wrap(item.details[lang], width - 2)) {
97
+ lines.push(` ${w}`);
98
+ }
99
+ lines.push("");
100
+ lines.push(` ${status} ${DIM}space toggle · enter confirm${RESET}`);
101
+ return lines.join("\n") + "\n";
102
+ };
103
+ const clear = () => {
104
+ if (lastLineCount > 0) {
105
+ process.stdout.write(`\x1b[${lastLineCount}A\x1b[J`);
106
+ }
107
+ };
108
+ const render = () => {
109
+ clear();
110
+ const text = view === "list" ? renderList() : renderDetails();
111
+ process.stdout.write(text);
112
+ lastLineCount = text.split("\n").length - 1;
113
+ };
114
+ const stdin = process.stdin;
115
+ stdin.setRawMode(true);
116
+ stdin.resume();
117
+ stdin.setEncoding("utf8");
118
+ process.stdout.write("\x1b[?25l"); // hide cursor
119
+ const cleanup = (clearUi) => {
120
+ process.stdout.write("\x1b[?25h");
121
+ stdin.setRawMode(false);
122
+ stdin.pause();
123
+ if (clearUi)
124
+ clear();
125
+ };
126
+ render();
127
+ try {
128
+ while (true) {
129
+ const chunk = await readKey();
130
+ if (chunk === "\x1b[A") {
131
+ cursor = (cursor - 1 + AGENTS.length) % AGENTS.length;
132
+ render();
133
+ continue;
134
+ }
135
+ if (chunk === "\x1b[B") {
136
+ cursor = (cursor + 1) % AGENTS.length;
137
+ render();
138
+ continue;
139
+ }
140
+ if (chunk === "\x1b[C") {
141
+ if (view === "list") {
142
+ view = "details";
143
+ render();
144
+ }
145
+ continue;
146
+ }
147
+ if (chunk === "\x1b[D" || chunk === "\x1b") {
148
+ if (view === "details") {
149
+ view = "list";
150
+ render();
151
+ }
152
+ continue;
153
+ }
154
+ if (chunk.startsWith("\x1b"))
155
+ continue;
156
+ for (const ch of chunk) {
157
+ if (ch === "\x03") {
158
+ cleanup(true);
159
+ process.stdout.write(`${YELLOW}aborted.${RESET}\n`);
160
+ process.exit(130);
161
+ }
162
+ if (ch === "\r" || ch === "\n") {
163
+ if (selected.size === 0 && options.requireAtLeastOne) {
164
+ errorMessage = "Select at least one agent before continuing.";
165
+ render();
166
+ continue;
167
+ }
168
+ cleanup(true);
169
+ const picked = AGENTS.filter((a) => selected.has(a.id));
170
+ const summary = picked.length > 0
171
+ ? picked.map((p) => p.label).join(", ")
172
+ : `${DIM}none${RESET}`;
173
+ process.stdout.write(`${CYAN}?${RESET} ${BOLD}${headerLabel}${RESET} ${DIM}›${RESET} ${summary}\n`);
174
+ return picked.map((p) => p.id);
175
+ }
176
+ if (ch === " ") {
177
+ // clear any prior "select at least one" warning once they interact
178
+ errorMessage = "";
179
+ const item = AGENTS[cursor];
180
+ if (disabled.has(item.id))
181
+ continue;
182
+ if (selected.has(item.id))
183
+ selected.delete(item.id);
184
+ else
185
+ selected.add(item.id);
186
+ render();
187
+ continue;
188
+ }
189
+ if (ch === "a") {
190
+ errorMessage = "";
191
+ for (const a of AGENTS) {
192
+ if (!disabled.has(a.id))
193
+ selected.add(a.id);
194
+ }
195
+ render();
196
+ continue;
197
+ }
198
+ if (ch === "n") {
199
+ // clearing all is allowed; if requireAtLeastOne, Enter will surface the warning.
200
+ selected.clear();
201
+ render();
202
+ continue;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ catch (err) {
208
+ cleanup(true);
209
+ throw err;
210
+ }
211
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export const CURRENT_CONFIG_VERSION = 1;
4
+ const CONFIG_PATH = "agentforge/config.json";
5
+ export const MASTER_DIR_REL = "agentforge/skills";
6
+ export function configPath(root) {
7
+ return join(root, CONFIG_PATH);
8
+ }
9
+ export function masterDir(root) {
10
+ return join(root, MASTER_DIR_REL);
11
+ }
12
+ /**
13
+ * Read the workspace config, or print a friendly error and exit if the
14
+ * directory hasn't been initialized as an agentforge workspace.
15
+ */
16
+ export function requireWorkspace(root) {
17
+ const cfg = readConfig(root);
18
+ if (cfg)
19
+ return cfg;
20
+ const YELLOW = "\x1b[33m";
21
+ const CYAN = "\x1b[36m";
22
+ const DIM = "\x1b[2m";
23
+ const RESET = "\x1b[0m";
24
+ process.stderr.write(`\n${YELLOW}⚠${RESET} Not an agentforge workspace yet.\n` +
25
+ ` ${DIM}${root}${RESET}\n\n` +
26
+ ` Run ${CYAN}agentforge init${RESET} here first to set one up.\n\n`);
27
+ process.exit(1);
28
+ }
29
+ export function readConfig(root) {
30
+ const p = configPath(root);
31
+ if (!existsSync(p))
32
+ return null;
33
+ // Guard against config.json being a directory (or other non-file).
34
+ try {
35
+ if (!statSync(p).isFile()) {
36
+ throw new Error(`${p} exists but is not a regular file — remove it and re-run \`agentforge init --force\``);
37
+ }
38
+ }
39
+ catch (e) {
40
+ throw new Error(`failed to read ${p}: ${e.message}`);
41
+ }
42
+ try {
43
+ const raw = JSON.parse(readFileSync(p, "utf8"));
44
+ if (!raw || typeof raw !== "object")
45
+ return null;
46
+ if (raw.version !== CURRENT_CONFIG_VERSION) {
47
+ // future: migrate
48
+ throw new Error(`unsupported agentforge/config.json version: ${raw.version}`);
49
+ }
50
+ const rawLang = raw.lang ?? "en";
51
+ if (rawLang !== "en" && rawLang !== "ko" && rawLang !== "ja") {
52
+ throw new Error(`unsupported lang "${rawLang}" — agentforge supports en | ko | ja. Fix \`lang:\` in agentforge/config.json or re-run \`agentforge init --force-skills\`.`);
53
+ }
54
+ const agents = Array.isArray(raw.agents)
55
+ ? raw.agents.filter((a) => a === "claude" || a === "cursor" || a === "codex")
56
+ : [];
57
+ return { version: 1, lang: rawLang, agents };
58
+ }
59
+ catch (err) {
60
+ throw new Error(`failed to read ${p}: ${err.message}`);
61
+ }
62
+ }
63
+ export function writeConfig(root, cfg) {
64
+ const p = configPath(root);
65
+ mkdirSync(dirname(p), { recursive: true });
66
+ writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`);
67
+ }
68
+ /** Merge a partial update into the existing config (or create a fresh one). */
69
+ export function upsertConfig(root, patch) {
70
+ const existing = readConfig(root);
71
+ const next = existing
72
+ ? {
73
+ version: 1,
74
+ lang: patch.lang ?? existing.lang,
75
+ agents: mergeAgents(existing.agents, patch.agents ?? []),
76
+ }
77
+ : {
78
+ version: 1,
79
+ lang: patch.lang ?? "en",
80
+ agents: patch.agents ?? [],
81
+ };
82
+ writeConfig(root, next);
83
+ return next;
84
+ }
85
+ /** Replace the agents list outright (used by remove-agent). */
86
+ export function setAgents(root, agents) {
87
+ const existing = readConfig(root);
88
+ const next = {
89
+ version: 1,
90
+ lang: existing?.lang ?? "en",
91
+ agents,
92
+ };
93
+ writeConfig(root, next);
94
+ return next;
95
+ }
96
+ function mergeAgents(prev, next) {
97
+ const seen = new Set();
98
+ const out = [];
99
+ for (const a of [...prev, ...next]) {
100
+ if (!seen.has(a)) {
101
+ seen.add(a);
102
+ out.push(a);
103
+ }
104
+ }
105
+ return out;
106
+ }
@@ -0,0 +1,46 @@
1
+ import { join } from "node:path";
2
+ import { ensureDir, renderTemplate, writeRendered } from "./io.js";
3
+ export const ClaudeAdapter = {
4
+ id: "claude",
5
+ label: "Claude Code",
6
+ outputSummary: ".claude/skills/ + CLAUDE.md",
7
+ details: {
8
+ en: [
9
+ "Writes `.claude/skills/<skill-id>/SKILL.md` for each master skill and a root `CLAUDE.md` guide.",
10
+ "",
11
+ "Auto-loaded by Claude Code from this workspace. The frontmatter `description` of each skill is what triggers it on matching natural-language prompts — strongest auto-matching of the three.",
12
+ ].join("\n"),
13
+ ko: [
14
+ "각 마스터 스킬을 `.claude/skills/<skill-id>/SKILL.md` 로, 워크스페이스 가이드를 `CLAUDE.md` 로 작성합니다.",
15
+ "",
16
+ "Claude Code 가 자동 로드. 각 스킬의 frontmatter `description` 이 자연어 요청에 자동 매칭됩니다 — 셋 중 가장 강한 자동 발동.",
17
+ ].join("\n"),
18
+ ja: [
19
+ "各マスタースキルを `.claude/skills/<skill-id>/SKILL.md` に、ワークスペースガイドを `CLAUDE.md` に書き出します。",
20
+ "",
21
+ "Claude Code が自動的に読み込みます。各スキルの frontmatter `description` が自然言語のリクエストに自動マッチします — 三つの中で最も強い自動発動。",
22
+ ].join("\n"),
23
+ },
24
+ install(params) {
25
+ const { root, masterSkills, forceSkills, forceClaude } = params;
26
+ ensureDir(join(root, ".claude/skills"), ".claude/skills");
27
+ for (const s of masterSkills) {
28
+ ensureDir(join(root, ".claude/skills", s.id), `.claude/skills/${s.id}`);
29
+ }
30
+ // root guide
31
+ const claudeMd = renderGuide(params);
32
+ writeRendered(join(root, "CLAUDE.md"), "CLAUDE.md", claudeMd, forceClaude);
33
+ // each skill — master raw content is already language-substituted; write as-is.
34
+ for (const s of masterSkills) {
35
+ const destRel = `.claude/skills/${s.id}/SKILL.md`;
36
+ writeRendered(join(root, destRel), destRel, s.raw, forceSkills);
37
+ }
38
+ },
39
+ };
40
+ /**
41
+ * Workspace guide for Claude Code, rendered from the package template
42
+ * (separate from master skills).
43
+ */
44
+ function renderGuide(params) {
45
+ return renderTemplate("CLAUDE.md.tpl", params.lang);
46
+ }
@@ -0,0 +1,67 @@
1
+ import { join } from "node:path";
2
+ import { ensureDir, renderTemplate, writeRendered } from "./io.js";
3
+ /**
4
+ * Convert a master skill into a standalone Codex skill file:
5
+ * - drop YAML frontmatter
6
+ * - top heading is `# <skill-id>`
7
+ * - second paragraph is the original description as plain text
8
+ * - then the body, untouched
9
+ */
10
+ function skillToCodex(skill) {
11
+ const description = skill.frontmatter["description"] ?? "";
12
+ return `# ${skill.id}\n\n${description}\n\n${skill.body}`;
13
+ }
14
+ /**
15
+ * Build AGENTS.md = workspace guide (from CLAUDE.md.tpl) + a Skills section that
16
+ * lists each master skill with its short description and reference path.
17
+ */
18
+ function buildAgentsMd(guideBody, skills) {
19
+ const skillsSection = [
20
+ "## Skills",
21
+ "",
22
+ "This workspace ships skill briefs under `.agents/skills/`. When a user request",
23
+ "matches a skill's purpose, load and follow that file. The skills:",
24
+ "",
25
+ ...skills.map((s) => `- **${s.id}** — ${s.frontmatter["description"] ?? ""}\n See \`.agents/skills/${s.id}.md\`.`),
26
+ "",
27
+ "Auto-discovery on this layout is best-effort. If a skill doesn't trigger,",
28
+ "say \"use the <skill-id> skill\" to invoke it explicitly.",
29
+ ].join("\n");
30
+ return [guideBody.trimEnd(), "", skillsSection, ""].join("\n");
31
+ }
32
+ export const CodexAdapter = {
33
+ id: "codex",
34
+ label: "OpenAI Codex CLI",
35
+ outputSummary: ".agents/skills/ + AGENTS.md",
36
+ details: {
37
+ en: [
38
+ "Writes `.agents/skills/<skill-id>.md` per master skill and a root `AGENTS.md` guide that lists each skill (description + reference path).",
39
+ "",
40
+ "Codex auto-loads AGENTS.md from the workspace root. From there it discovers the skill briefs by reference. Auto-matching is best-effort — say \"use the <skill-id> skill\" if a trigger phrase doesn't catch.",
41
+ ].join("\n"),
42
+ ko: [
43
+ "각 마스터 스킬을 `.agents/skills/<skill-id>.md` 로, 워크스페이스 가이드를 `AGENTS.md` 로 작성 (가이드에 각 스킬 description + reference path 나열).",
44
+ "",
45
+ "Codex 가 AGENTS.md 를 워크스페이스 루트에서 자동 로드. 거기서 referenced 파일들을 발견하는 흐름. 자동 매칭은 best-effort — 발동 안 되면 \"use the <skill-id> skill\" 식으로 명시.",
46
+ ].join("\n"),
47
+ ja: [
48
+ "各マスタースキルを `.agents/skills/<skill-id>.md` に、ワークスペースガイドを `AGENTS.md` に書き出します (ガイドに各スキルの description + reference path を列挙)。",
49
+ "",
50
+ "Codex は AGENTS.md をワークスペースルートから自動ロード。そこから referenced ファイルを発見する流れ。自動マッチングは best-effort — 発動しない場合は「use the <skill-id> skill」と明示。",
51
+ ].join("\n"),
52
+ },
53
+ install(params) {
54
+ const { root, masterSkills, lang, forceSkills, forceClaude } = params;
55
+ ensureDir(join(root, ".agents/skills"), ".agents/skills");
56
+ // 1) per-skill files
57
+ for (const s of masterSkills) {
58
+ const codexSkill = skillToCodex(s);
59
+ const destRel = `.agents/skills/${s.id}.md`;
60
+ writeRendered(join(root, destRel), destRel, codexSkill, forceSkills);
61
+ }
62
+ // 2) AGENTS.md guide with Skills directory
63
+ const guideBody = renderTemplate("CLAUDE.md.tpl", lang);
64
+ const content = buildAgentsMd(guideBody, masterSkills);
65
+ writeRendered(join(root, "AGENTS.md"), "AGENTS.md", content, forceClaude);
66
+ },
67
+ };
@@ -0,0 +1,54 @@
1
+ import { join } from "node:path";
2
+ import { ensureDir, renderTemplate, writeRendered } from "./io.js";
3
+ /**
4
+ * Convert a master skill into a Cursor MDC rule:
5
+ * - drop `name:` (filename serves as the identifier)
6
+ * - keep `description:` for Cursor 1.0+ auto-matching on intent
7
+ * - add `alwaysApply: false` (safe default for older Cursors)
8
+ * - body unchanged
9
+ */
10
+ function skillToMdc(skill) {
11
+ const description = skill.frontmatter["description"] ?? "";
12
+ const fmLines = [
13
+ "---",
14
+ `description: ${description}`,
15
+ "alwaysApply: false",
16
+ "---",
17
+ "",
18
+ ];
19
+ return fmLines.join("\n") + skill.body;
20
+ }
21
+ export const CursorAdapter = {
22
+ id: "cursor",
23
+ label: "Cursor",
24
+ outputSummary: ".cursor/rules/ + .cursorrules",
25
+ details: {
26
+ en: [
27
+ "Writes `.cursor/rules/<skill-id>.mdc` per master skill and a root `.cursorrules` guide.",
28
+ "",
29
+ "Each MDC keeps the original description so Cursor 1.0+ can auto-match on intent. `alwaysApply: false` keeps older Cursors safe (rule loads on request, not always).",
30
+ ].join("\n"),
31
+ ko: [
32
+ "각 마스터 스킬을 `.cursor/rules/<skill-id>.mdc` 로, 워크스페이스 가이드를 `.cursorrules` 로 작성합니다.",
33
+ "",
34
+ "MDC 가 원본 description 을 유지하므로 Cursor 1.0+ 이 의도 기반 자동 매칭. `alwaysApply: false` 로 옛 Cursor 에서도 안전 (요청 시에만 로드).",
35
+ ].join("\n"),
36
+ ja: [
37
+ "各マスタースキルを `.cursor/rules/<skill-id>.mdc` に、ワークスペースガイドを `.cursorrules` に書き出します。",
38
+ "",
39
+ "MDC は元の description を保持するため Cursor 1.0+ が意図ベースで自動マッチ。`alwaysApply: false` で古い Cursor でも安全 (要求時のみロード)。",
40
+ ].join("\n"),
41
+ },
42
+ install(params) {
43
+ const { root, masterSkills, lang, forceSkills, forceClaude } = params;
44
+ ensureDir(join(root, ".cursor/rules"), ".cursor/rules");
45
+ // root guide → .cursorrules (re-use the CLAUDE.md.tpl body)
46
+ writeRendered(join(root, ".cursorrules"), ".cursorrules", renderTemplate("CLAUDE.md.tpl", lang), forceClaude);
47
+ // each master skill → .mdc
48
+ for (const s of masterSkills) {
49
+ const mdc = skillToMdc(s);
50
+ const destRel = `.cursor/rules/${s.id}.mdc`;
51
+ writeRendered(join(root, destRel), destRel, mdc, forceSkills);
52
+ }
53
+ },
54
+ };
@@ -0,0 +1,15 @@
1
+ import { ClaudeAdapter } from "./claude.js";
2
+ import { CodexAdapter } from "./codex.js";
3
+ import { CursorAdapter } from "./cursor.js";
4
+ export const AGENTS = [
5
+ ClaudeAdapter,
6
+ CursorAdapter,
7
+ CodexAdapter,
8
+ ];
9
+ export const AGENT_IDS = AGENTS.map((a) => a.id);
10
+ export function getAgent(id) {
11
+ const a = AGENTS.find((x) => x.id === id);
12
+ if (!a)
13
+ throw new Error(`unknown agent id: ${id}`);
14
+ return a;
15
+ }