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,26 @@
1
+ const BLOCK_START = "<!-- nymor:start -->";
2
+ const BLOCK_END = "<!-- nymor:end -->";
3
+
4
+ export function renderManagedBlock(content: string): string {
5
+ const body = content.trimEnd();
6
+ return `${BLOCK_START}\n${body}\n${BLOCK_END}`;
7
+ }
8
+
9
+ export function upsertManagedBlock(existing: string | null, content: string): string {
10
+ const block = renderManagedBlock(content);
11
+ if (!existing) {
12
+ return `${block}\n`;
13
+ }
14
+
15
+ const pattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`);
16
+ if (pattern.test(existing)) {
17
+ return existing.replace(pattern, block);
18
+ }
19
+
20
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
21
+ return `${existing}${separator}${block}\n`;
22
+ }
23
+
24
+ function escapeRegExp(value: string): string {
25
+ return value.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
26
+ }
@@ -0,0 +1,13 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { SkillFile } from "../utils/skills";
4
+
5
+ export async function compileClaudeSkills(skills: SkillFile[], projectRoot: string): Promise<void> {
6
+ const outputRoot = path.join(projectRoot, ".claude", "skills");
7
+ await fs.ensureDir(outputRoot);
8
+
9
+ for (const skill of skills) {
10
+ const targetDir = path.join(outputRoot, skill.id);
11
+ await fs.copy(skill.dirPath, targetDir, { overwrite: true });
12
+ }
13
+ }
@@ -0,0 +1,28 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { SkillFile } from "../utils/skills";
4
+
5
+ export async function compileCopilotSkills(skills: SkillFile[], projectRoot: string): Promise<void> {
6
+ const outputRoot = path.join(projectRoot, ".github", "instructions");
7
+ await fs.ensureDir(outputRoot);
8
+
9
+ for (const skill of skills) {
10
+ const fileName = `nymor-${skill.id}.instructions.md`;
11
+ const outputPath = path.join(outputRoot, fileName);
12
+ const content = renderCopilotInstructions(skill);
13
+ await fs.writeFile(outputPath, content, "utf8");
14
+ }
15
+ }
16
+
17
+ export function renderCopilotInstructions(skill: SkillFile): string {
18
+ const globs = skill.frontmatter.globs ?? [];
19
+ const applyTo = globs.length > 0 ? globs.join(", ") : "**/*";
20
+ const lines = ["---", `applyTo: ${formatYamlValue(applyTo)}`, "---", ""];
21
+
22
+ return `${lines.join("\n")}\n${skill.body.trimStart()}\n`;
23
+ }
24
+
25
+ function formatYamlValue(value: string): string {
26
+ const escaped = value.replace(/"/g, "\\\"");
27
+ return `"${escaped}"`;
28
+ }
@@ -0,0 +1,38 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { SkillFile } from "../utils/skills";
4
+
5
+ export async function compileCursorSkills(skills: SkillFile[], projectRoot: string): Promise<void> {
6
+ const outputRoot = path.join(projectRoot, ".cursor", "rules");
7
+ await fs.ensureDir(outputRoot);
8
+
9
+ for (const skill of skills) {
10
+ const fileName = `nymor-${skill.id}.mdc`;
11
+ const outputPath = path.join(outputRoot, fileName);
12
+ const content = renderCursorRule(skill);
13
+ await fs.writeFile(outputPath, content, "utf8");
14
+ }
15
+ }
16
+
17
+ export function renderCursorRule(skill: SkillFile): string {
18
+ const description = skill.frontmatter.description || skill.frontmatter.name;
19
+ const globs = skill.frontmatter.globs ?? [];
20
+ const alwaysApply = Boolean(skill.frontmatter.alwaysApply);
21
+ const lines: string[] = ["---", `description: ${formatYamlValue(description)}`];
22
+
23
+ if (globs.length > 0) {
24
+ lines.push("globs:");
25
+ globs.forEach((glob) => lines.push(` - ${formatYamlValue(glob)}`));
26
+ } else {
27
+ lines.push("globs: []");
28
+ }
29
+
30
+ lines.push(`alwaysApply: ${alwaysApply}`, "---", "");
31
+
32
+ return `${lines.join("\n")}\n${skill.body.trimStart()}\n`;
33
+ }
34
+
35
+ function formatYamlValue(value: string): string {
36
+ const escaped = value.replace(/"/g, "\\\"");
37
+ return `"${escaped}"`;
38
+ }
@@ -0,0 +1,22 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { SkillFile } from "../utils/skills";
4
+
5
+ export async function compileKiroSkills(skills: SkillFile[], projectRoot: string): Promise<void> {
6
+ const outputRoot = path.join(projectRoot, ".kiro", "steering");
7
+ await fs.ensureDir(outputRoot);
8
+
9
+ for (const skill of skills) {
10
+ const fileName = `nymor-${skill.id}.md`;
11
+ const outputPath = path.join(outputRoot, fileName);
12
+ const content = renderKiroSteering(skill);
13
+ await fs.writeFile(outputPath, content, "utf8");
14
+ }
15
+ }
16
+
17
+ export function renderKiroSteering(skill: SkillFile): string {
18
+ const inclusion = skill.frontmatter.alwaysApply ? "always" : "manual";
19
+ const lines = ["---", `inclusion: ${inclusion}`, "---", ""];
20
+
21
+ return `${lines.join("\n")}\n${skill.body.trimStart()}\n`;
22
+ }
@@ -0,0 +1,26 @@
1
+ import path from "path";
2
+ import fs from "fs-extra";
3
+ import { AGENT_TARGETS, AgentTarget } from "../agents/targets";
4
+
5
+ export type AgentPresence = Record<AgentTarget, boolean>;
6
+
7
+ export async function detectAgents(projectRoot: string): Promise<AgentPresence> {
8
+ const entries = await Promise.all(
9
+ AGENT_TARGETS.map(async (target) => {
10
+ const detected = await hasAnyPath(projectRoot, target.detectPaths);
11
+ return [target.id, detected] as const;
12
+ })
13
+ );
14
+
15
+ return Object.fromEntries(entries) as AgentPresence;
16
+ }
17
+
18
+ async function hasAnyPath(projectRoot: string, relativePaths: string[]): Promise<boolean> {
19
+ for (const relativePath of relativePaths) {
20
+ if (await fs.pathExists(path.join(projectRoot, relativePath))) {
21
+ return true;
22
+ }
23
+ }
24
+
25
+ return false;
26
+ }
@@ -0,0 +1,135 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { glob } from "glob";
4
+
5
+ export type Stack = "nodejs" | "react" | "vue" | "fullstack" | "python" | "django" | "fastapi" | "rust" | "go";
6
+
7
+ const NODE_DEPENDENCIES = new Set(["express", "fastify", "koa", "hapi", "@nestjs/core"]);
8
+ const REACT_DEPENDENCIES = new Set(["react", "next", "@remix-run/react"]);
9
+ const VUE_DEPENDENCIES = new Set(["vue", "nuxt", "@nuxt/schema"]);
10
+ const FULLSTACK_DEPENDENCIES = new Set(["next", "@remix-run/react"]);
11
+
12
+ const SIGNALS: Record<Stack, string[]> = {
13
+ nodejs: ["server.js", "server.ts", "src/server.ts", "src/server.js"],
14
+ react: ["src/App.tsx", "src/App.jsx", "next.config.js", "next.config.ts"],
15
+ vue: ["vue.config.js", "nuxt.config.js", "nuxt.config.ts", "vite.config.ts"],
16
+ fullstack: ["next.config.js", "next.config.ts", "remix.config.js"],
17
+ python: ["requirements.txt", "pyproject.toml", "setup.py"],
18
+ django: ["manage.py"],
19
+ fastapi: ["main.py", "app/main.py"],
20
+ rust: ["Cargo.toml"],
21
+ go: ["go.mod"]
22
+ };
23
+
24
+ export async function detectStack(projectRoot: string): Promise<Stack | null> {
25
+ const packageDependencies = await readPackageDependencies(projectRoot);
26
+ const pythonManifest = await readPythonManifest(projectRoot);
27
+ const detected = new Set<Stack>();
28
+
29
+ if (hasAnyDependency(packageDependencies, FULLSTACK_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.fullstack))) {
30
+ detected.add("fullstack");
31
+ }
32
+
33
+ if (hasAnyDependency(packageDependencies, REACT_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.react))) {
34
+ detected.add("react");
35
+ }
36
+
37
+ if (hasAnyDependency(packageDependencies, VUE_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.vue))) {
38
+ detected.add("vue");
39
+ }
40
+
41
+ if (hasAnyDependency(packageDependencies, NODE_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.nodejs))) {
42
+ detected.add("nodejs");
43
+ }
44
+
45
+ if ((await hasAnySignal(projectRoot, SIGNALS.django)) && hasPythonDependency(pythonManifest, "django")) {
46
+ detected.add("django");
47
+ }
48
+
49
+ if (hasPythonDependency(pythonManifest, "fastapi") || (await hasAnySignal(projectRoot, SIGNALS.fastapi))) {
50
+ detected.add("fastapi");
51
+ }
52
+
53
+ if (pythonManifest || (await hasAnySignal(projectRoot, SIGNALS.python))) {
54
+ detected.add("python");
55
+ }
56
+
57
+ if (await hasAnySignal(projectRoot, SIGNALS.rust)) {
58
+ detected.add("rust");
59
+ }
60
+
61
+ if (await hasAnySignal(projectRoot, SIGNALS.go)) {
62
+ detected.add("go");
63
+ }
64
+
65
+ if (detected.has("django") || detected.has("fastapi")) {
66
+ detected.delete("python");
67
+ }
68
+
69
+ if (detected.size === 0) {
70
+ return null;
71
+ }
72
+
73
+ if (detected.has("fullstack") || detected.size > 1) {
74
+ return "fullstack";
75
+ }
76
+
77
+ return [...detected][0];
78
+ }
79
+
80
+ async function readPackageDependencies(projectRoot: string): Promise<Set<string>> {
81
+ const packageJsonPath = path.join(projectRoot, "package.json");
82
+ if (!(await fs.pathExists(packageJsonPath))) {
83
+ return new Set();
84
+ }
85
+
86
+ try {
87
+ const pkg = await fs.readJson(packageJsonPath);
88
+ return new Set(Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }));
89
+ } catch {
90
+ return new Set();
91
+ }
92
+ }
93
+
94
+ async function readPythonManifest(projectRoot: string): Promise<string | null> {
95
+ const files = ["requirements.txt", "pyproject.toml", "setup.py"];
96
+ const contents: string[] = [];
97
+
98
+ for (const file of files) {
99
+ const filePath = path.join(projectRoot, file);
100
+ if (await fs.pathExists(filePath)) {
101
+ contents.push(await fs.readFile(filePath, "utf8"));
102
+ }
103
+ }
104
+
105
+ return contents.length > 0 ? contents.join("\n").toLowerCase() : null;
106
+ }
107
+
108
+ function hasAnyDependency(actual: Set<string>, expected: Set<string>): boolean {
109
+ return [...expected].some((dependency) => actual.has(dependency));
110
+ }
111
+
112
+ function hasPythonDependency(manifest: string | null, dependency: string): boolean {
113
+ if (!manifest) {
114
+ return false;
115
+ }
116
+
117
+ return new RegExp(`(^|[^a-z0-9_-])${dependency}([^a-z0-9_-]|$)`, "i").test(manifest);
118
+ }
119
+
120
+ async function hasAnySignal(projectRoot: string, patterns: string[]): Promise<boolean> {
121
+ for (const pattern of patterns) {
122
+ const matches = await glob(pattern, {
123
+ cwd: projectRoot,
124
+ nodir: true,
125
+ dot: true,
126
+ ignore: ["**/node_modules/**", "**/.git/**"]
127
+ });
128
+
129
+ if (matches.length > 0) {
130
+ return true;
131
+ }
132
+ }
133
+
134
+ return false;
135
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { initCommand } from "./commands/init";
4
+ import { compileCommand } from "./commands/compile";
5
+ import { learnCommand } from "./commands/learn";
6
+ import { listCommand } from "./commands/list";
7
+ import { doctorCommand } from "./commands/doctor";
8
+ import { validateCommand } from "./commands/validate";
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name("nymor")
14
+ .description("Teach your repo what your AI agents keep forgetting")
15
+ .version("2.0.0");
16
+
17
+ program
18
+ .command("init")
19
+ .description("Initialize Nymor in the current repo")
20
+ .action(() => initCommand());
21
+
22
+ program
23
+ .command("compile")
24
+ .description("Compile skills to agent surfaces")
25
+ .action(() => compileCommand());
26
+
27
+ program
28
+ .command("learn", { hidden: true })
29
+ .argument("<rule>", "One-line rule or convention to capture")
30
+ .option("--id <id>", "Local skill folder id")
31
+ .option("--name <name>", "Skill name")
32
+ .option("--description <description>", "Skill description")
33
+ .option("--globs <globs>", "Comma-separated file globs")
34
+ .option("--always-apply", "Apply the skill regardless of globs")
35
+ .option("--why <why>", "Why this rule matters")
36
+ .option("--example <example>", "Example or counter-example")
37
+ .description("Internal fallback for capturing a project rule as a local skill")
38
+ .action((rule, options) => learnCommand(rule, options));
39
+
40
+ program
41
+ .command("list")
42
+ .description("List active repo skills")
43
+ .action(() => listCommand());
44
+
45
+ program
46
+ .command("doctor")
47
+ .description("Check skills for common issues")
48
+ .action(() => doctorCommand());
49
+
50
+ program
51
+ .command("validate")
52
+ .description("Validate skill file format and index entries")
53
+ .action(() => validateCommand());
54
+
55
+ program.parseAsync(process.argv).catch((err) => {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ console.error(message);
58
+ process.exitCode = 1;
59
+ });
@@ -0,0 +1,109 @@
1
+ import { AgentTargetDefinition } from "../agents/targets";
2
+ import { SkillFile } from "../utils/skills";
3
+
4
+ const LEARN_TRIGGER = "/nymor-learn";
5
+
6
+ function getLearnArgumentsPlaceholder(target: AgentTargetDefinition): string {
7
+ return target.id === "copilot" ? "{{input}}" : "$ARGUMENTS";
8
+ }
9
+
10
+ export function renderBootstrap(target: AgentTargetDefinition, skills: SkillFile[] = [], wrap = true): string {
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
+
43
+ return wrap ? wrapBootstrap(target, body) : body;
44
+ }
45
+
46
+ export function renderLearnCommand(target: AgentTargetDefinition): string {
47
+ const inputPlaceholder = getLearnArgumentsPlaceholder(target);
48
+
49
+ return [
50
+ "---",
51
+ "description: Capture a durable repo rule as a Nymor skill.",
52
+ "---",
53
+ "",
54
+ `# ${target.label} Nymor Learn`,
55
+ "",
56
+ "User input:",
57
+ "",
58
+ "```text",
59
+ inputPlaceholder,
60
+ "```",
61
+ "",
62
+ renderBootstrap(target, [], false)
63
+ ].join("\n");
64
+ }
65
+
66
+ export function renderLearnSkill(target: AgentTargetDefinition): string {
67
+ return [
68
+ "---",
69
+ "name: Nymor Learn",
70
+ "description: Capture durable repo rules as active Nymor skills.",
71
+ "globs:",
72
+ " - \"**/*\"",
73
+ "alwaysApply: true",
74
+ "---",
75
+ "",
76
+ renderBootstrap(target, [], false)
77
+ ].join("\n");
78
+ }
79
+
80
+ function wrapBootstrap(target: AgentTargetDefinition, body: string): string {
81
+ if (target.id === "cursor") {
82
+ return ["---", "description: Nymor repo-memory capture and skill guidance.", "globs:", " - \"**/*\"", "alwaysApply: true", "---", "", body].join(
83
+ "\n"
84
+ );
85
+ }
86
+
87
+ if (target.id === "copilot") {
88
+ return ["---", "applyTo: \"**/*\"", "---", "", body].join("\n");
89
+ }
90
+
91
+ if (target.id === "kiro") {
92
+ return ["---", "inclusion: always", "---", "", body].join("\n");
93
+ }
94
+
95
+ return body;
96
+ }
97
+
98
+ function renderAlwaysApplySection(skills: SkillFile[]): string {
99
+ const alwaysApply = skills.filter((skill) => skill.frontmatter.alwaysApply);
100
+ if (alwaysApply.length === 0) {
101
+ return "No always-apply Nymor skills are currently configured.";
102
+ }
103
+
104
+ return [
105
+ "## Always-Apply Nymor Skills",
106
+ "",
107
+ ...alwaysApply.map((skill) => [`### ${skill.frontmatter.name}`, "", skill.body.trim()].join("\n")).join("\n\n---\n\n").split("\n")
108
+ ].join("\n");
109
+ }
@@ -0,0 +1,15 @@
1
+ import { DEFAULT_AGENT_TARGETS } from "../agents/targets";
2
+
3
+ export interface NymorManifestTemplate {
4
+ version: string;
5
+ agents: string[];
6
+ local: string[];
7
+ }
8
+
9
+ export function createDefaultManifest(): NymorManifestTemplate {
10
+ return {
11
+ version: "1",
12
+ agents: [...DEFAULT_AGENT_TARGETS],
13
+ local: []
14
+ };
15
+ }
@@ -0,0 +1,38 @@
1
+ import fs from "fs-extra";
2
+ import { AgentTarget, isAgentTarget } from "../agents/targets";
3
+ import { createDefaultManifest } from "../templates/nymor-json";
4
+ import { getManifestPath } from "./paths";
5
+
6
+ export interface NymorManifest {
7
+ version: string;
8
+ agents: AgentTarget[];
9
+ local: string[];
10
+ }
11
+
12
+ export async function readManifest(projectRoot: string): Promise<NymorManifest> {
13
+ const manifestPath = getManifestPath(projectRoot);
14
+ if (!(await fs.pathExists(manifestPath))) {
15
+ return createDefaultManifest() as NymorManifest;
16
+ }
17
+
18
+ const manifest = await fs.readJson(manifestPath);
19
+ return normalizeManifest(manifest);
20
+ }
21
+
22
+ export async function writeManifest(projectRoot: string, manifest: NymorManifest): Promise<void> {
23
+ const manifestPath = getManifestPath(projectRoot);
24
+ await fs.writeJson(manifestPath, manifest, { spaces: 2 });
25
+ }
26
+
27
+ function normalizeManifest(raw: Partial<NymorManifest>): NymorManifest {
28
+ const defaultManifest = createDefaultManifest();
29
+ const agents = (raw.agents ?? defaultManifest.agents).filter((agent): agent is AgentTarget =>
30
+ isAgentTarget(String(agent))
31
+ );
32
+
33
+ return {
34
+ version: raw.version ?? defaultManifest.version,
35
+ agents,
36
+ local: raw.local ?? []
37
+ };
38
+ }
@@ -0,0 +1,25 @@
1
+ import path from "path";
2
+
3
+ export function getRepoRoot(cwd: string = process.cwd()): string {
4
+ return cwd;
5
+ }
6
+
7
+ export function getNymorDir(root: string): string {
8
+ return path.join(root, ".nymor");
9
+ }
10
+
11
+ export function getSkillsDir(root: string): string {
12
+ return path.join(getNymorDir(root), "skills");
13
+ }
14
+
15
+ export function getIndexMarkdownPath(root: string): string {
16
+ return path.join(getNymorDir(root), "index.md");
17
+ }
18
+
19
+ export function getIndexJsonPath(root: string): string {
20
+ return path.join(getNymorDir(root), "index.json");
21
+ }
22
+
23
+ export function getManifestPath(root: string): string {
24
+ return path.join(root, "nymor.json");
25
+ }