nymor 1.0.1

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -0
  3. package/dist/agents/targets.js +111 -0
  4. package/dist/commands/add.js +121 -0
  5. package/dist/commands/compile.js +159 -0
  6. package/dist/commands/doctor.js +205 -0
  7. package/dist/commands/init.js +98 -0
  8. package/dist/commands/inject.js +24 -0
  9. package/dist/commands/learn.js +145 -0
  10. package/dist/commands/list.js +55 -0
  11. package/dist/commands/remove.js +38 -0
  12. package/dist/commands/update.js +80 -0
  13. package/dist/commands/validate.js +82 -0
  14. package/dist/compiler/agentsmd.js +17 -0
  15. package/dist/compiler/block.js +25 -0
  16. package/dist/compiler/claude.js +16 -0
  17. package/dist/compiler/copilot.js +29 -0
  18. package/dist/compiler/cursor.js +38 -0
  19. package/dist/compiler/kiro.js +24 -0
  20. package/dist/detector/agents.js +24 -0
  21. package/dist/detector/stack.js +113 -0
  22. package/dist/index.js +52 -0
  23. package/dist/registry/cache.js +60 -0
  24. package/dist/registry/client.js +135 -0
  25. package/dist/registry/resolver.js +29 -0
  26. package/dist/registry/types.js +2 -0
  27. package/dist/templates/bootstrap.js +97 -0
  28. package/dist/templates/cicada-json.js +11 -0
  29. package/dist/templates/nymor-json.js +11 -0
  30. package/dist/utils/manifest.js +32 -0
  31. package/dist/utils/paths.js +30 -0
  32. package/dist/utils/skills.js +114 -0
  33. package/package.json +32 -0
  34. package/src/agents/targets.ts +141 -0
  35. package/src/commands/compile.ts +202 -0
  36. package/src/commands/doctor.ts +253 -0
  37. package/src/commands/init.ts +113 -0
  38. package/src/commands/learn.ts +175 -0
  39. package/src/commands/list.ts +57 -0
  40. package/src/commands/validate.ts +89 -0
  41. package/src/compiler/block.ts +26 -0
  42. package/src/compiler/claude.ts +13 -0
  43. package/src/compiler/copilot.ts +28 -0
  44. package/src/compiler/cursor.ts +38 -0
  45. package/src/compiler/kiro.ts +22 -0
  46. package/src/detector/agents.ts +26 -0
  47. package/src/detector/stack.ts +135 -0
  48. package/src/index.ts +59 -0
  49. package/src/templates/bootstrap.ts +109 -0
  50. package/src/templates/nymor-json.ts +15 -0
  51. package/src/utils/manifest.ts +38 -0
  52. package/src/utils/paths.ts +25 -0
  53. package/src/utils/skills.ts +152 -0
  54. package/tests/compiler/__snapshots__/claude.test.ts.snap +65 -0
  55. package/tests/compiler/__snapshots__/copilot.test.ts.snap +54 -0
  56. package/tests/compiler/__snapshots__/cursor.test.ts.snap +62 -0
  57. package/tests/compiler/__snapshots__/kiro.test.ts.snap +54 -0
  58. package/tests/compiler/block.test.ts +24 -0
  59. package/tests/compiler/claude.test.ts +46 -0
  60. package/tests/compiler/copilot.test.ts +15 -0
  61. package/tests/compiler/cursor.test.ts +15 -0
  62. package/tests/compiler/kiro.test.ts +15 -0
  63. package/tests/detector/agents.test.ts +48 -0
  64. package/tests/detector/stack.test.ts +29 -0
  65. package/tests/e2e/init-and-compile.test.ts +227 -0
  66. package/tests/fixtures/skills/scoped/SKILL.md +18 -0
  67. package/tests/fixtures/skills/simple/SKILL.md +16 -0
  68. package/tests/fixtures/skills/with-examples/SKILL.md +18 -0
  69. package/tests/fixtures/skills/with-examples/examples/example.md +3 -0
  70. package/tests/fixtures/stacks/django/manage.py +2 -0
  71. package/tests/fixtures/stacks/django/requirements.txt +1 -0
  72. package/tests/fixtures/stacks/fastapi/requirements.txt +1 -0
  73. package/tests/fixtures/stacks/go/go.mod +3 -0
  74. package/tests/fixtures/stacks/nodejs/package.json +5 -0
  75. package/tests/fixtures/stacks/react/package.json +5 -0
  76. package/tests/fixtures/stacks/rust/Cargo.toml +4 -0
  77. package/tests/fixtures/stacks/vue/package.json +8 -0
  78. package/tests/fixtures/stacks/vue/vite.config.ts +5 -0
  79. package/tests/utils/manifest.test.ts +31 -0
  80. package/tests/utils/paths.test.ts +23 -0
  81. package/tests/utils/skills.test.ts +49 -0
  82. package/tsconfig.json +14 -0
  83. package/vitest.config.ts +8 -0
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderBootstrap = renderBootstrap;
4
+ exports.renderLearnCommand = renderLearnCommand;
5
+ exports.renderLearnSkill = renderLearnSkill;
6
+ const LEARN_TRIGGER = "/nymor-learn";
7
+ function getLearnArgumentsPlaceholder(target) {
8
+ return target.id === "copilot" ? "{{input}}" : "$ARGUMENTS";
9
+ }
10
+ function renderBootstrap(target, skills = [], wrap = true) {
11
+ const body = [
12
+ "# Nymor",
13
+ "",
14
+ "Nymor teaches this repo what AI agents keep forgetting.",
15
+ "",
16
+ "Source skills live in `.nymor/skills/`. Apply any matching skill during the task, using `globs` and `alwaysApply` to decide relevance.",
17
+ "",
18
+ "If the user states a durable repo rule, says things like \"always\", \"never\", \"in this repo\", \"we use\", \"remember this\", or corrects the same behavior more than once, gently suggest:",
19
+ "",
20
+ "`This looks like a reusable repo rule. Want me to capture it with /nymor-learn?`",
21
+ "",
22
+ `Only create a skill after the user explicitly invokes ${LEARN_TRIGGER}.`,
23
+ "",
24
+ `When the user invokes ${LEARN_TRIGGER} "<rule>":`,
25
+ "",
26
+ "1. Infer the durable rule from the conversation.",
27
+ "2. Choose a stable lowercase folder id with hyphens.",
28
+ "3. Read `nymor.json` and `.nymor/skills/`.",
29
+ "4. Write `.nymor/skills/<id>/SKILL.md` directly.",
30
+ "5. Include complete frontmatter: `name`, `description`, `globs`, and `alwaysApply`.",
31
+ "6. Include complete sections: `## Rule`, `## Why`, and `## Example`.",
32
+ "7. Keep `globs` narrow unless the rule is genuinely global.",
33
+ "8. Add `<id>` to `nymor.json.local` if missing.",
34
+ "9. Run `nymor compile`.",
35
+ "10. Run `nymor validate`.",
36
+ "11. Report the captured rule and skill path.",
37
+ "",
38
+ "Never create placeholders. Write directly to the active skill path. Create only the requested skill; do not seed unrelated skills or use external installs.",
39
+ "",
40
+ renderAlwaysApplySection(skills)
41
+ ].join("\n");
42
+ return wrap ? wrapBootstrap(target, body) : body;
43
+ }
44
+ function renderLearnCommand(target) {
45
+ const inputPlaceholder = getLearnArgumentsPlaceholder(target);
46
+ return [
47
+ "---",
48
+ "description: Capture a durable repo rule as a Nymor skill.",
49
+ "---",
50
+ "",
51
+ `# ${target.label} Nymor Learn`,
52
+ "",
53
+ "User input:",
54
+ "",
55
+ "```text",
56
+ inputPlaceholder,
57
+ "```",
58
+ "",
59
+ renderBootstrap(target, [], false)
60
+ ].join("\n");
61
+ }
62
+ function renderLearnSkill(target) {
63
+ return [
64
+ "---",
65
+ "name: Nymor Learn",
66
+ "description: Capture durable repo rules as active Nymor skills.",
67
+ "globs:",
68
+ " - \"**/*\"",
69
+ "alwaysApply: true",
70
+ "---",
71
+ "",
72
+ renderBootstrap(target, [], false)
73
+ ].join("\n");
74
+ }
75
+ function wrapBootstrap(target, body) {
76
+ if (target.id === "cursor") {
77
+ return ["---", "description: Nymor repo-memory capture and skill guidance.", "globs:", " - \"**/*\"", "alwaysApply: true", "---", "", body].join("\n");
78
+ }
79
+ if (target.id === "copilot") {
80
+ return ["---", "applyTo: \"**/*\"", "---", "", body].join("\n");
81
+ }
82
+ if (target.id === "kiro") {
83
+ return ["---", "inclusion: always", "---", "", body].join("\n");
84
+ }
85
+ return body;
86
+ }
87
+ function renderAlwaysApplySection(skills) {
88
+ const alwaysApply = skills.filter((skill) => skill.frontmatter.alwaysApply);
89
+ if (alwaysApply.length === 0) {
90
+ return "No always-apply Nymor skills are currently configured.";
91
+ }
92
+ return [
93
+ "## Always-Apply Nymor Skills",
94
+ "",
95
+ ...alwaysApply.map((skill) => [`### ${skill.frontmatter.name}`, "", skill.body.trim()].join("\n")).join("\n\n---\n\n").split("\n")
96
+ ].join("\n");
97
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDefaultManifest = createDefaultManifest;
4
+ function createDefaultManifest() {
5
+ return {
6
+ version: "1",
7
+ agents: ["claude", "cursor", "copilot", "kiro", "agents-md"],
8
+ skills: {},
9
+ local: []
10
+ };
11
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDefaultManifest = createDefaultManifest;
4
+ const targets_1 = require("../agents/targets");
5
+ function createDefaultManifest() {
6
+ return {
7
+ version: "1",
8
+ agents: [...targets_1.DEFAULT_AGENT_TARGETS],
9
+ local: []
10
+ };
11
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readManifest = readManifest;
7
+ exports.writeManifest = writeManifest;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const targets_1 = require("../agents/targets");
10
+ const nymor_json_1 = require("../templates/nymor-json");
11
+ const paths_1 = require("./paths");
12
+ async function readManifest(projectRoot) {
13
+ const manifestPath = (0, paths_1.getManifestPath)(projectRoot);
14
+ if (!(await fs_extra_1.default.pathExists(manifestPath))) {
15
+ return (0, nymor_json_1.createDefaultManifest)();
16
+ }
17
+ const manifest = await fs_extra_1.default.readJson(manifestPath);
18
+ return normalizeManifest(manifest);
19
+ }
20
+ async function writeManifest(projectRoot, manifest) {
21
+ const manifestPath = (0, paths_1.getManifestPath)(projectRoot);
22
+ await fs_extra_1.default.writeJson(manifestPath, manifest, { spaces: 2 });
23
+ }
24
+ function normalizeManifest(raw) {
25
+ const defaultManifest = (0, nymor_json_1.createDefaultManifest)();
26
+ const agents = (raw.agents ?? defaultManifest.agents).filter((agent) => (0, targets_1.isAgentTarget)(String(agent)));
27
+ return {
28
+ version: raw.version ?? defaultManifest.version,
29
+ agents,
30
+ local: raw.local ?? []
31
+ };
32
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getRepoRoot = getRepoRoot;
7
+ exports.getNymorDir = getNymorDir;
8
+ exports.getSkillsDir = getSkillsDir;
9
+ exports.getIndexMarkdownPath = getIndexMarkdownPath;
10
+ exports.getIndexJsonPath = getIndexJsonPath;
11
+ exports.getManifestPath = getManifestPath;
12
+ const path_1 = __importDefault(require("path"));
13
+ function getRepoRoot(cwd = process.cwd()) {
14
+ return cwd;
15
+ }
16
+ function getNymorDir(root) {
17
+ return path_1.default.join(root, ".nymor");
18
+ }
19
+ function getSkillsDir(root) {
20
+ return path_1.default.join(getNymorDir(root), "skills");
21
+ }
22
+ function getIndexMarkdownPath(root) {
23
+ return path_1.default.join(getNymorDir(root), "index.md");
24
+ }
25
+ function getIndexJsonPath(root) {
26
+ return path_1.default.join(getNymorDir(root), "index.json");
27
+ }
28
+ function getManifestPath(root) {
29
+ return path_1.default.join(root, "nymor.json");
30
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listSkillDirectories = listSkillDirectories;
7
+ exports.loadSkills = loadSkills;
8
+ exports.readSkillFile = readSkillFile;
9
+ exports.parseSkillContent = parseSkillContent;
10
+ exports.buildSkillIndex = buildSkillIndex;
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const yaml_1 = __importDefault(require("yaml"));
14
+ async function listSkillDirectories(skillsDir) {
15
+ if (!(await fs_extra_1.default.pathExists(skillsDir))) {
16
+ return [];
17
+ }
18
+ const entries = await fs_extra_1.default.readdir(skillsDir, { withFileTypes: true });
19
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
20
+ }
21
+ async function loadSkills(skillsDir) {
22
+ const skillDirs = await listSkillDirectories(skillsDir);
23
+ const skills = [];
24
+ for (const dirName of skillDirs) {
25
+ const skillPath = path_1.default.join(skillsDir, dirName, "SKILL.md");
26
+ if (!(await fs_extra_1.default.pathExists(skillPath))) {
27
+ continue;
28
+ }
29
+ skills.push(await readSkillFile(dirName, skillPath));
30
+ }
31
+ return skills.sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
32
+ }
33
+ async function readSkillFile(id, skillPath) {
34
+ const raw = await fs_extra_1.default.readFile(skillPath, "utf8");
35
+ const { frontmatter, body } = parseSkillContent(raw, id);
36
+ return {
37
+ id,
38
+ dirPath: path_1.default.dirname(skillPath),
39
+ skillPath,
40
+ frontmatter,
41
+ body,
42
+ raw
43
+ };
44
+ }
45
+ function parseSkillContent(content, id) {
46
+ const lines = content.split(/\r?\n/);
47
+ if (lines.length === 0 || lines[0].trim() !== "---") {
48
+ throw new Error(`Skill ${id} is missing frontmatter`);
49
+ }
50
+ const endIndex = lines.slice(1).findIndex((line) => line.trim() === "---");
51
+ if (endIndex === -1) {
52
+ throw new Error(`Skill ${id} frontmatter is not closed`);
53
+ }
54
+ const frontmatterText = lines.slice(1, endIndex + 1).join("\n");
55
+ const body = lines.slice(endIndex + 2).join("\n").trimStart();
56
+ const data = yaml_1.default.parse(frontmatterText);
57
+ if (!data?.name) {
58
+ throw new Error(`Skill ${id} is missing a name in frontmatter`);
59
+ }
60
+ const globs = normalizeGlobs(data.globs);
61
+ return {
62
+ frontmatter: {
63
+ name: data.name,
64
+ description: data.description ?? "",
65
+ globs,
66
+ alwaysApply: Boolean(data.alwaysApply)
67
+ },
68
+ body
69
+ };
70
+ }
71
+ function buildSkillIndex(skills) {
72
+ const entries = skills.map(toIndexEntry);
73
+ const rows = entries.map((entry) => {
74
+ const globs = entry.globs.length > 0 ? entry.globs.join(", ") : "-";
75
+ const alwaysApply = entry.alwaysApply ? "yes" : "no";
76
+ return `| ${entry.name} | ${entry.id} | ${entry.description || "-"} | ${globs} | ${alwaysApply} |`;
77
+ });
78
+ const markdown = [
79
+ "# Nymor Skills Index",
80
+ "",
81
+ "This index is regenerated by Nymor on every compile.",
82
+ "",
83
+ "| Skill | Folder | Description | Globs | Always Apply |",
84
+ "|---|---|---|---|---|",
85
+ ...rows,
86
+ "",
87
+ "## How to use this index",
88
+ "1. Review the Description and Globs columns",
89
+ "2. Load the relevant skill folders from .nymor/skills/",
90
+ "3. Apply their rules for the current task",
91
+ ""
92
+ ].join("\n");
93
+ const json = JSON.stringify({ skills: entries }, null, 2);
94
+ return { markdown, json };
95
+ }
96
+ function toIndexEntry(skill) {
97
+ const globs = normalizeGlobs(skill.frontmatter.globs);
98
+ return {
99
+ id: skill.id,
100
+ name: skill.frontmatter.name,
101
+ description: skill.frontmatter.description ?? "",
102
+ globs,
103
+ alwaysApply: Boolean(skill.frontmatter.alwaysApply)
104
+ };
105
+ }
106
+ function normalizeGlobs(globs) {
107
+ if (!globs) {
108
+ return [];
109
+ }
110
+ if (Array.isArray(globs)) {
111
+ return globs.map((glob) => String(glob)).filter(Boolean);
112
+ }
113
+ return [String(globs)];
114
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "nymor",
3
+ "version": "1.0.1",
4
+ "description": "Teach your repo what your AI agents keep forgetting",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "nymor": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "start": "node dist/index.js",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest"
15
+ },
16
+ "dependencies": {
17
+ "commander": "^11.0.0",
18
+ "fs-extra": "^11.0.0",
19
+ "glob": "^10.0.0",
20
+ "inquirer": "^9.0.0",
21
+ "picocolors": "^1.0.0",
22
+ "yaml": "^2.5.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/fs-extra": "^11.0.0",
26
+ "@types/inquirer": "^9.0.0",
27
+ "@types/node": "^20.0.0",
28
+ "ts-node": "^10.0.0",
29
+ "typescript": "^5.0.0",
30
+ "vitest": "^1.6.1"
31
+ }
32
+ }
@@ -0,0 +1,141 @@
1
+ import path from "path";
2
+
3
+ export type AgentTarget =
4
+ | "claude"
5
+ | "cursor"
6
+ | "copilot"
7
+ | "kiro"
8
+ | "agents-md"
9
+ | "gemini"
10
+ | "windsurf"
11
+ | "goose"
12
+ | "opencode";
13
+
14
+ export type AgentOutputKind =
15
+ | "claude"
16
+ | "cursor"
17
+ | "copilot"
18
+ | "kiro"
19
+ | "shared-md"
20
+ | "gemini"
21
+ | "windsurf"
22
+ | "native-skills";
23
+
24
+ export interface AgentTargetDefinition {
25
+ id: AgentTarget;
26
+ label: string;
27
+ short: string;
28
+ description: string;
29
+ detectPaths: string[];
30
+ kind: AgentOutputKind;
31
+ bootstrapFile?: string;
32
+ commandFile?: string;
33
+ nativeSkillDir?: string;
34
+ sharedConsumers?: string[];
35
+ }
36
+
37
+ export const AGENT_TARGETS: AgentTargetDefinition[] = [
38
+ {
39
+ id: "claude",
40
+ label: "Claude Code",
41
+ short: "Claude",
42
+ description: "Claude skills, CLAUDE.md bootstrap, and /nymor-learn command",
43
+ detectPaths: [".claude", "CLAUDE.md"],
44
+ kind: "claude",
45
+ bootstrapFile: "CLAUDE.md",
46
+ commandFile: path.join(".claude", "commands", "nymor-learn.md")
47
+ },
48
+ {
49
+ id: "cursor",
50
+ label: "Cursor",
51
+ short: "Cursor",
52
+ description: "Cursor rules and /nymor-learn command",
53
+ detectPaths: [".cursor"],
54
+ kind: "cursor",
55
+ bootstrapFile: path.join(".cursor", "rules", "nymor.mdc"),
56
+ commandFile: path.join(".cursor", "commands", "nymor-learn.md")
57
+ },
58
+ {
59
+ id: "copilot",
60
+ label: "GitHub Copilot",
61
+ short: "Copilot",
62
+ description: "GitHub Copilot instructions and /nymor-learn prompt",
63
+ detectPaths: [
64
+ path.join(".github", "copilot-instructions.md"),
65
+ path.join(".github", "instructions"),
66
+ path.join(".github", "prompts")
67
+ ],
68
+ kind: "copilot",
69
+ bootstrapFile: path.join(".github", "instructions", "nymor-bootstrap.instructions.md"),
70
+ commandFile: path.join(".github", "prompts", "nymor-learn.prompt.md")
71
+ },
72
+ {
73
+ id: "kiro",
74
+ label: "Kiro",
75
+ short: "Kiro",
76
+ description: "Kiro steering files",
77
+ detectPaths: [".kiro"],
78
+ kind: "kiro",
79
+ bootstrapFile: path.join(".kiro", "steering", "nymor.md")
80
+ },
81
+ {
82
+ id: "agents-md",
83
+ label: "AGENTS.md",
84
+ short: "AGENTS.md",
85
+ description: "Shared AGENTS.md for Codex, OpenCode, Aider, Goose, Zed, Warp, Devin, and Junie",
86
+ detectPaths: ["AGENTS.md", ".codex", ".aider.conf.yml", ".zed", ".warp", ".junie", ".goose", ".opencode"],
87
+ kind: "shared-md",
88
+ bootstrapFile: "AGENTS.md",
89
+ sharedConsumers: ["Codex", "OpenCode", "Aider", "Goose", "Zed", "Warp", "Devin", "Junie"]
90
+ },
91
+ {
92
+ id: "gemini",
93
+ label: "Gemini CLI",
94
+ short: "Gemini",
95
+ description: "GEMINI.md managed block",
96
+ detectPaths: ["GEMINI.md", ".gemini"],
97
+ kind: "gemini",
98
+ bootstrapFile: "GEMINI.md"
99
+ },
100
+ {
101
+ id: "windsurf",
102
+ label: "Windsurf",
103
+ short: "Windsurf",
104
+ description: "Windsurf project rule file",
105
+ detectPaths: [".windsurf", ".windsurfrules"],
106
+ kind: "windsurf",
107
+ bootstrapFile: path.join(".windsurf", "rules", "nymor.md")
108
+ },
109
+ {
110
+ id: "goose",
111
+ label: "Goose",
112
+ short: "Goose",
113
+ description: "Goose native skills",
114
+ detectPaths: [".goose"],
115
+ kind: "native-skills",
116
+ nativeSkillDir: path.join(".goose", "skills")
117
+ },
118
+ {
119
+ id: "opencode",
120
+ label: "OpenCode",
121
+ short: "OpenCode",
122
+ description: "OpenCode native skills",
123
+ detectPaths: [".opencode", "opencode.json"],
124
+ kind: "native-skills",
125
+ nativeSkillDir: path.join(".opencode", "skill")
126
+ }
127
+ ];
128
+
129
+ export const DEFAULT_AGENT_TARGETS: AgentTarget[] = AGENT_TARGETS.map((target) => target.id);
130
+
131
+ export function getAgentTargetDefinition(id: AgentTarget): AgentTargetDefinition {
132
+ const target = AGENT_TARGETS.find((item) => item.id === id);
133
+ if (!target) {
134
+ throw new Error(`Unknown agent target: ${id}`);
135
+ }
136
+ return target;
137
+ }
138
+
139
+ export function isAgentTarget(value: string): value is AgentTarget {
140
+ return AGENT_TARGETS.some((target) => target.id === value);
141
+ }
@@ -0,0 +1,202 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { AGENT_TARGETS, AgentTargetDefinition } from "../agents/targets";
4
+ import { renderCopilotInstructions } from "../compiler/copilot";
5
+ import { renderCursorRule } from "../compiler/cursor";
6
+ import { renderKiroSteering } from "../compiler/kiro";
7
+ import { upsertManagedBlock } from "../compiler/block";
8
+ import { renderBootstrap, renderLearnCommand, renderLearnSkill } from "../templates/bootstrap";
9
+ import { buildSkillIndex, loadSkills, SkillFile } from "../utils/skills";
10
+ import { readManifest } from "../utils/manifest";
11
+ import { getIndexJsonPath, getIndexMarkdownPath, getNymorDir, getSkillsDir } from "../utils/paths";
12
+
13
+ export interface PlannedCompileFile {
14
+ path: string;
15
+ content: Buffer;
16
+ }
17
+
18
+ export async function compileCommand(): Promise<void> {
19
+ const projectRoot = process.cwd();
20
+ const skillsDir = getSkillsDir(projectRoot);
21
+
22
+ if (!(await fs.pathExists(skillsDir))) {
23
+ console.log("No skills found. Run nymor init first.");
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+
28
+ const skills = await loadSkills(skillsDir);
29
+ const { markdown, json } = buildSkillIndex(skills);
30
+
31
+ await fs.ensureDir(getNymorDir(projectRoot));
32
+ await fs.writeFile(getIndexMarkdownPath(projectRoot), markdown, "utf8");
33
+ await fs.writeFile(getIndexJsonPath(projectRoot), json, "utf8");
34
+
35
+ const manifest = await readManifest(projectRoot);
36
+ const agentSet = new Set(manifest.agents);
37
+
38
+ for (const target of AGENT_TARGETS) {
39
+ if (agentSet.has(target.id)) {
40
+ await writeTargetOutputs(projectRoot, target, skills);
41
+ }
42
+ }
43
+
44
+ console.log(`Compiled ${skills.length} skills.`);
45
+ }
46
+
47
+ export async function planCompileOutputs(projectRoot: string): Promise<PlannedCompileFile[]> {
48
+ const skillsDir = getSkillsDir(projectRoot);
49
+ const skills = await loadSkills(skillsDir);
50
+ const { markdown, json } = buildSkillIndex(skills);
51
+ const manifest = await readManifest(projectRoot);
52
+ const agentSet = new Set(manifest.agents);
53
+ const files: PlannedCompileFile[] = [
54
+ textFile(getIndexMarkdownPath(projectRoot), markdown),
55
+ textFile(getIndexJsonPath(projectRoot), json)
56
+ ];
57
+
58
+ for (const target of AGENT_TARGETS) {
59
+ if (agentSet.has(target.id)) {
60
+ files.push(...(await planTargetOutputs(projectRoot, target, skills)));
61
+ }
62
+ }
63
+
64
+ return files;
65
+ }
66
+
67
+ async function writeTargetOutputs(projectRoot: string, target: AgentTargetDefinition, skills: SkillFile[]): Promise<void> {
68
+ for (const file of await planTargetOutputs(projectRoot, target, skills)) {
69
+ await fs.ensureDir(path.dirname(file.path));
70
+ await fs.writeFile(file.path, file.content);
71
+ }
72
+ }
73
+
74
+ async function planTargetOutputs(
75
+ projectRoot: string,
76
+ target: AgentTargetDefinition,
77
+ skills: SkillFile[]
78
+ ): Promise<PlannedCompileFile[]> {
79
+ const files: PlannedCompileFile[] = [];
80
+
81
+ if (target.bootstrapFile) {
82
+ files.push(await planBootstrap(projectRoot, target, skills));
83
+ }
84
+
85
+ if (target.commandFile) {
86
+ files.push(textFile(path.join(projectRoot, target.commandFile), `${renderLearnCommand(target)}\n`));
87
+ }
88
+
89
+ switch (target.kind) {
90
+ case "claude":
91
+ files.push(...(await planClaudeOutputs(skills, projectRoot)));
92
+ break;
93
+ case "cursor":
94
+ for (const skill of skills) {
95
+ files.push(textFile(path.join(projectRoot, ".cursor", "rules", `nymor-${skill.id}.mdc`), renderCursorRule(skill)));
96
+ }
97
+ break;
98
+ case "copilot":
99
+ for (const skill of skills) {
100
+ files.push(
101
+ textFile(
102
+ path.join(projectRoot, ".github", "instructions", `nymor-${skill.id}.instructions.md`),
103
+ renderCopilotInstructions(skill)
104
+ )
105
+ );
106
+ }
107
+ break;
108
+ case "kiro":
109
+ for (const skill of skills) {
110
+ files.push(textFile(path.join(projectRoot, ".kiro", "steering", `nymor-${skill.id}.md`), renderKiroSteering(skill)));
111
+ }
112
+ break;
113
+ case "native-skills":
114
+ if (target.nativeSkillDir) {
115
+ files.push(...(await planNativeSkillOutputs(projectRoot, target, skills)));
116
+ }
117
+ break;
118
+ case "shared-md":
119
+ case "gemini":
120
+ case "windsurf":
121
+ break;
122
+ }
123
+
124
+ return files;
125
+ }
126
+
127
+ async function planBootstrap(
128
+ projectRoot: string,
129
+ target: AgentTargetDefinition,
130
+ skills: SkillFile[]
131
+ ): Promise<PlannedCompileFile> {
132
+ const targetPath = path.join(projectRoot, target.bootstrapFile!);
133
+ const content = renderBootstrap(target, skills);
134
+
135
+ if (target.id === "claude" || target.id === "agents-md" || target.id === "gemini") {
136
+ const existing = (await fs.pathExists(targetPath)) ? await fs.readFile(targetPath, "utf8") : null;
137
+ return textFile(targetPath, upsertManagedBlock(existing, content));
138
+ }
139
+
140
+ return textFile(targetPath, `${content.trimEnd()}\n`);
141
+ }
142
+
143
+ async function planClaudeOutputs(skills: SkillFile[], projectRoot: string): Promise<PlannedCompileFile[]> {
144
+ const outputRoot = path.join(projectRoot, ".claude", "skills");
145
+ const files: PlannedCompileFile[] = [];
146
+
147
+ for (const skill of skills) {
148
+ const sourceFiles = await listFilesRecursive(skill.dirPath);
149
+ for (const sourcePath of sourceFiles) {
150
+ const relative = path.relative(skill.dirPath, sourcePath);
151
+ files.push({
152
+ path: path.join(outputRoot, skill.id, relative),
153
+ content: await fs.readFile(sourcePath)
154
+ });
155
+ }
156
+ }
157
+
158
+ return files;
159
+ }
160
+
161
+ async function planNativeSkillOutputs(
162
+ projectRoot: string,
163
+ target: AgentTargetDefinition,
164
+ skills: SkillFile[]
165
+ ): Promise<PlannedCompileFile[]> {
166
+ const files: PlannedCompileFile[] = [
167
+ textFile(path.join(projectRoot, target.nativeSkillDir!, "nymor-learn", "SKILL.md"), `${renderLearnSkill(target)}\n`)
168
+ ];
169
+
170
+ for (const skill of skills) {
171
+ const sourceFiles = await listFilesRecursive(skill.dirPath);
172
+ for (const sourcePath of sourceFiles) {
173
+ const relative = path.relative(skill.dirPath, sourcePath);
174
+ files.push({
175
+ path: path.join(projectRoot, target.nativeSkillDir!, skill.id, relative),
176
+ content: await fs.readFile(sourcePath)
177
+ });
178
+ }
179
+ }
180
+
181
+ return files;
182
+ }
183
+
184
+ async function listFilesRecursive(root: string): Promise<string[]> {
185
+ const entries = await fs.readdir(root, { withFileTypes: true });
186
+ const files: string[] = [];
187
+
188
+ for (const entry of entries) {
189
+ const entryPath = path.join(root, entry.name);
190
+ if (entry.isDirectory()) {
191
+ files.push(...(await listFilesRecursive(entryPath)));
192
+ } else if (entry.isFile()) {
193
+ files.push(entryPath);
194
+ }
195
+ }
196
+
197
+ return files.sort();
198
+ }
199
+
200
+ function textFile(filePath: string, content: string): PlannedCompileFile {
201
+ return { path: filePath, content: Buffer.from(content, "utf8") };
202
+ }