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,82 @@
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.validateCommand = validateCommand;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const paths_1 = require("../utils/paths");
10
+ const skills_1 = require("../utils/skills");
11
+ async function validateCommand() {
12
+ const projectRoot = process.cwd();
13
+ const skillsDir = (0, paths_1.getSkillsDir)(projectRoot);
14
+ const indexJsonPath = (0, paths_1.getIndexJsonPath)(projectRoot);
15
+ if (!(await fs_extra_1.default.pathExists(skillsDir))) {
16
+ console.log("No skills found. Run nymor init first.");
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ const indexExists = await fs_extra_1.default.pathExists(indexJsonPath);
21
+ const indexEntries = indexExists ? await fs_extra_1.default.readJson(indexJsonPath) : { skills: [] };
22
+ const indexIds = new Set(Array.isArray(indexEntries.skills) ? indexEntries.skills.map((entry) => entry.id) : []);
23
+ const skillDirs = await (0, skills_1.listSkillDirectories)(skillsDir);
24
+ console.log("Validating skills...\n");
25
+ let errorCount = 0;
26
+ const okMark = "\u2713";
27
+ const errMark = "\u2717";
28
+ for (const dirName of skillDirs) {
29
+ const skillPath = path_1.default.join(skillsDir, dirName, "SKILL.md");
30
+ const errors = [];
31
+ if (!(await fs_extra_1.default.pathExists(skillPath))) {
32
+ errors.push("missing SKILL.md");
33
+ }
34
+ else {
35
+ const content = await fs_extra_1.default.readFile(skillPath, "utf8");
36
+ try {
37
+ const { frontmatter, body } = (0, skills_1.parseSkillContent)(content, dirName);
38
+ if (!frontmatter.name) {
39
+ errors.push("missing frontmatter name");
40
+ }
41
+ if (!hasSection(body, "Rule")) {
42
+ errors.push("missing ## Rule section");
43
+ }
44
+ if (!hasSection(body, "Why")) {
45
+ errors.push("missing ## Why section");
46
+ }
47
+ if (!hasSection(body, "Example")) {
48
+ errors.push("missing ## Example section");
49
+ }
50
+ }
51
+ catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ errors.push(message);
54
+ }
55
+ }
56
+ if (indexExists && !indexIds.has(dirName)) {
57
+ errors.push("not found in index.json");
58
+ }
59
+ if (errors.length === 0) {
60
+ console.log(` ${okMark} ${dirName}`);
61
+ }
62
+ else {
63
+ errorCount += 1;
64
+ console.log(` ${errMark} ${dirName} - ${errors.join("; ")}`);
65
+ }
66
+ }
67
+ if (!indexExists) {
68
+ errorCount += 1;
69
+ console.log("\nIndex not found. Run nymor compile to regenerate index.json.");
70
+ }
71
+ if (errorCount > 0) {
72
+ console.log(`\n${errorCount} issues found. Fix them or run nymor compile again.`);
73
+ process.exitCode = 1;
74
+ }
75
+ else {
76
+ console.log("\nAll skills look good.");
77
+ }
78
+ }
79
+ function hasSection(content, heading) {
80
+ const regex = new RegExp(`^##\\s+${heading}\\b`, "m");
81
+ return regex.test(content);
82
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderAgentsMarkdown = renderAgentsMarkdown;
4
+ function renderAgentsMarkdown(skills) {
5
+ const alwaysApply = skills.filter((skill) => skill.frontmatter.alwaysApply);
6
+ if (alwaysApply.length === 0) {
7
+ return "No always-apply skills are currently configured.";
8
+ }
9
+ const blocks = alwaysApply.map((skill) => skill.body.trim());
10
+ return [
11
+ "# Nymor Agents",
12
+ "",
13
+ "This file is generated by nymor compile.",
14
+ "",
15
+ ...blocks.join("\n\n---\n\n").split("\n")
16
+ ].join("\n");
17
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderManagedBlock = renderManagedBlock;
4
+ exports.upsertManagedBlock = upsertManagedBlock;
5
+ const BLOCK_START = "<!-- nymor:start -->";
6
+ const BLOCK_END = "<!-- nymor:end -->";
7
+ function renderManagedBlock(content) {
8
+ const body = content.trimEnd();
9
+ return `${BLOCK_START}\n${body}\n${BLOCK_END}`;
10
+ }
11
+ function upsertManagedBlock(existing, content) {
12
+ const block = renderManagedBlock(content);
13
+ if (!existing) {
14
+ return `${block}\n`;
15
+ }
16
+ const pattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`);
17
+ if (pattern.test(existing)) {
18
+ return existing.replace(pattern, block);
19
+ }
20
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
21
+ return `${existing}${separator}${block}\n`;
22
+ }
23
+ function escapeRegExp(value) {
24
+ return value.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
25
+ }
@@ -0,0 +1,16 @@
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.compileClaudeSkills = compileClaudeSkills;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ async function compileClaudeSkills(skills, projectRoot) {
10
+ const outputRoot = path_1.default.join(projectRoot, ".claude", "skills");
11
+ await fs_extra_1.default.ensureDir(outputRoot);
12
+ for (const skill of skills) {
13
+ const targetDir = path_1.default.join(outputRoot, skill.id);
14
+ await fs_extra_1.default.copy(skill.dirPath, targetDir, { overwrite: true });
15
+ }
16
+ }
@@ -0,0 +1,29 @@
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.compileCopilotSkills = compileCopilotSkills;
7
+ exports.renderCopilotInstructions = renderCopilotInstructions;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ async function compileCopilotSkills(skills, projectRoot) {
11
+ const outputRoot = path_1.default.join(projectRoot, ".github", "instructions");
12
+ await fs_extra_1.default.ensureDir(outputRoot);
13
+ for (const skill of skills) {
14
+ const fileName = `nymor-${skill.id}.instructions.md`;
15
+ const outputPath = path_1.default.join(outputRoot, fileName);
16
+ const content = renderCopilotInstructions(skill);
17
+ await fs_extra_1.default.writeFile(outputPath, content, "utf8");
18
+ }
19
+ }
20
+ function renderCopilotInstructions(skill) {
21
+ const globs = skill.frontmatter.globs ?? [];
22
+ const applyTo = globs.length > 0 ? globs.join(", ") : "**/*";
23
+ const lines = ["---", `applyTo: ${formatYamlValue(applyTo)}`, "---", ""];
24
+ return `${lines.join("\n")}\n${skill.body.trimStart()}\n`;
25
+ }
26
+ function formatYamlValue(value) {
27
+ const escaped = value.replace(/"/g, "\\\"");
28
+ return `"${escaped}"`;
29
+ }
@@ -0,0 +1,38 @@
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.compileCursorSkills = compileCursorSkills;
7
+ exports.renderCursorRule = renderCursorRule;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ async function compileCursorSkills(skills, projectRoot) {
11
+ const outputRoot = path_1.default.join(projectRoot, ".cursor", "rules");
12
+ await fs_extra_1.default.ensureDir(outputRoot);
13
+ for (const skill of skills) {
14
+ const fileName = `nymor-${skill.id}.mdc`;
15
+ const outputPath = path_1.default.join(outputRoot, fileName);
16
+ const content = renderCursorRule(skill);
17
+ await fs_extra_1.default.writeFile(outputPath, content, "utf8");
18
+ }
19
+ }
20
+ function renderCursorRule(skill) {
21
+ const description = skill.frontmatter.description || skill.frontmatter.name;
22
+ const globs = skill.frontmatter.globs ?? [];
23
+ const alwaysApply = Boolean(skill.frontmatter.alwaysApply);
24
+ const lines = ["---", `description: ${formatYamlValue(description)}`];
25
+ if (globs.length > 0) {
26
+ lines.push("globs:");
27
+ globs.forEach((glob) => lines.push(` - ${formatYamlValue(glob)}`));
28
+ }
29
+ else {
30
+ lines.push("globs: []");
31
+ }
32
+ lines.push(`alwaysApply: ${alwaysApply}`, "---", "");
33
+ return `${lines.join("\n")}\n${skill.body.trimStart()}\n`;
34
+ }
35
+ function formatYamlValue(value) {
36
+ const escaped = value.replace(/"/g, "\\\"");
37
+ return `"${escaped}"`;
38
+ }
@@ -0,0 +1,24 @@
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.compileKiroSkills = compileKiroSkills;
7
+ exports.renderKiroSteering = renderKiroSteering;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ async function compileKiroSkills(skills, projectRoot) {
11
+ const outputRoot = path_1.default.join(projectRoot, ".kiro", "steering");
12
+ await fs_extra_1.default.ensureDir(outputRoot);
13
+ for (const skill of skills) {
14
+ const fileName = `nymor-${skill.id}.md`;
15
+ const outputPath = path_1.default.join(outputRoot, fileName);
16
+ const content = renderKiroSteering(skill);
17
+ await fs_extra_1.default.writeFile(outputPath, content, "utf8");
18
+ }
19
+ }
20
+ function renderKiroSteering(skill) {
21
+ const inclusion = skill.frontmatter.alwaysApply ? "always" : "manual";
22
+ const lines = ["---", `inclusion: ${inclusion}`, "---", ""];
23
+ return `${lines.join("\n")}\n${skill.body.trimStart()}\n`;
24
+ }
@@ -0,0 +1,24 @@
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.detectAgents = detectAgents;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const targets_1 = require("../agents/targets");
10
+ async function detectAgents(projectRoot) {
11
+ const entries = await Promise.all(targets_1.AGENT_TARGETS.map(async (target) => {
12
+ const detected = await hasAnyPath(projectRoot, target.detectPaths);
13
+ return [target.id, detected];
14
+ }));
15
+ return Object.fromEntries(entries);
16
+ }
17
+ async function hasAnyPath(projectRoot, relativePaths) {
18
+ for (const relativePath of relativePaths) {
19
+ if (await fs_extra_1.default.pathExists(path_1.default.join(projectRoot, relativePath))) {
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
@@ -0,0 +1,113 @@
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.detectStack = detectStack;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ const NODE_DEPENDENCIES = new Set(["express", "fastify", "koa", "hapi", "@nestjs/core"]);
11
+ const REACT_DEPENDENCIES = new Set(["react", "next", "@remix-run/react"]);
12
+ const VUE_DEPENDENCIES = new Set(["vue", "nuxt", "@nuxt/schema"]);
13
+ const FULLSTACK_DEPENDENCIES = new Set(["next", "@remix-run/react"]);
14
+ const SIGNALS = {
15
+ nodejs: ["server.js", "server.ts", "src/server.ts", "src/server.js"],
16
+ react: ["src/App.tsx", "src/App.jsx", "next.config.js", "next.config.ts"],
17
+ vue: ["vue.config.js", "nuxt.config.js", "nuxt.config.ts", "vite.config.ts"],
18
+ fullstack: ["next.config.js", "next.config.ts", "remix.config.js"],
19
+ python: ["requirements.txt", "pyproject.toml", "setup.py"],
20
+ django: ["manage.py"],
21
+ fastapi: ["main.py", "app/main.py"],
22
+ rust: ["Cargo.toml"],
23
+ go: ["go.mod"]
24
+ };
25
+ async function detectStack(projectRoot) {
26
+ const packageDependencies = await readPackageDependencies(projectRoot);
27
+ const pythonManifest = await readPythonManifest(projectRoot);
28
+ const detected = new Set();
29
+ if (hasAnyDependency(packageDependencies, FULLSTACK_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.fullstack))) {
30
+ detected.add("fullstack");
31
+ }
32
+ if (hasAnyDependency(packageDependencies, REACT_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.react))) {
33
+ detected.add("react");
34
+ }
35
+ if (hasAnyDependency(packageDependencies, VUE_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.vue))) {
36
+ detected.add("vue");
37
+ }
38
+ if (hasAnyDependency(packageDependencies, NODE_DEPENDENCIES) || (await hasAnySignal(projectRoot, SIGNALS.nodejs))) {
39
+ detected.add("nodejs");
40
+ }
41
+ if ((await hasAnySignal(projectRoot, SIGNALS.django)) && hasPythonDependency(pythonManifest, "django")) {
42
+ detected.add("django");
43
+ }
44
+ if (hasPythonDependency(pythonManifest, "fastapi") || (await hasAnySignal(projectRoot, SIGNALS.fastapi))) {
45
+ detected.add("fastapi");
46
+ }
47
+ if (pythonManifest || (await hasAnySignal(projectRoot, SIGNALS.python))) {
48
+ detected.add("python");
49
+ }
50
+ if (await hasAnySignal(projectRoot, SIGNALS.rust)) {
51
+ detected.add("rust");
52
+ }
53
+ if (await hasAnySignal(projectRoot, SIGNALS.go)) {
54
+ detected.add("go");
55
+ }
56
+ if (detected.has("django") || detected.has("fastapi")) {
57
+ detected.delete("python");
58
+ }
59
+ if (detected.size === 0) {
60
+ return null;
61
+ }
62
+ if (detected.has("fullstack") || detected.size > 1) {
63
+ return "fullstack";
64
+ }
65
+ return [...detected][0];
66
+ }
67
+ async function readPackageDependencies(projectRoot) {
68
+ const packageJsonPath = path_1.default.join(projectRoot, "package.json");
69
+ if (!(await fs_extra_1.default.pathExists(packageJsonPath))) {
70
+ return new Set();
71
+ }
72
+ try {
73
+ const pkg = await fs_extra_1.default.readJson(packageJsonPath);
74
+ return new Set(Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }));
75
+ }
76
+ catch {
77
+ return new Set();
78
+ }
79
+ }
80
+ async function readPythonManifest(projectRoot) {
81
+ const files = ["requirements.txt", "pyproject.toml", "setup.py"];
82
+ const contents = [];
83
+ for (const file of files) {
84
+ const filePath = path_1.default.join(projectRoot, file);
85
+ if (await fs_extra_1.default.pathExists(filePath)) {
86
+ contents.push(await fs_extra_1.default.readFile(filePath, "utf8"));
87
+ }
88
+ }
89
+ return contents.length > 0 ? contents.join("\n").toLowerCase() : null;
90
+ }
91
+ function hasAnyDependency(actual, expected) {
92
+ return [...expected].some((dependency) => actual.has(dependency));
93
+ }
94
+ function hasPythonDependency(manifest, dependency) {
95
+ if (!manifest) {
96
+ return false;
97
+ }
98
+ return new RegExp(`(^|[^a-z0-9_-])${dependency}([^a-z0-9_-]|$)`, "i").test(manifest);
99
+ }
100
+ async function hasAnySignal(projectRoot, patterns) {
101
+ for (const pattern of patterns) {
102
+ const matches = await (0, glob_1.glob)(pattern, {
103
+ cwd: projectRoot,
104
+ nodir: true,
105
+ dot: true,
106
+ ignore: ["**/node_modules/**", "**/.git/**"]
107
+ });
108
+ if (matches.length > 0) {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ }
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const init_1 = require("./commands/init");
6
+ const compile_1 = require("./commands/compile");
7
+ const learn_1 = require("./commands/learn");
8
+ const list_1 = require("./commands/list");
9
+ const doctor_1 = require("./commands/doctor");
10
+ const validate_1 = require("./commands/validate");
11
+ const program = new commander_1.Command();
12
+ program
13
+ .name("nymor")
14
+ .description("Teach your repo what your AI agents keep forgetting")
15
+ .version("2.0.0");
16
+ program
17
+ .command("init")
18
+ .description("Initialize Nymor in the current repo")
19
+ .action(() => (0, init_1.initCommand)());
20
+ program
21
+ .command("compile")
22
+ .description("Compile skills to agent surfaces")
23
+ .action(() => (0, compile_1.compileCommand)());
24
+ program
25
+ .command("learn", { hidden: true })
26
+ .argument("<rule>", "One-line rule or convention to capture")
27
+ .option("--id <id>", "Local skill folder id")
28
+ .option("--name <name>", "Skill name")
29
+ .option("--description <description>", "Skill description")
30
+ .option("--globs <globs>", "Comma-separated file globs")
31
+ .option("--always-apply", "Apply the skill regardless of globs")
32
+ .option("--why <why>", "Why this rule matters")
33
+ .option("--example <example>", "Example or counter-example")
34
+ .description("Internal fallback for capturing a project rule as a local skill")
35
+ .action((rule, options) => (0, learn_1.learnCommand)(rule, options));
36
+ program
37
+ .command("list")
38
+ .description("List active repo skills")
39
+ .action(() => (0, list_1.listCommand)());
40
+ program
41
+ .command("doctor")
42
+ .description("Check skills for common issues")
43
+ .action(() => (0, doctor_1.doctorCommand)());
44
+ program
45
+ .command("validate")
46
+ .description("Validate skill file format and index entries")
47
+ .action(() => (0, validate_1.validateCommand)());
48
+ program.parseAsync(process.argv).catch((err) => {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ console.error(message);
51
+ process.exitCode = 1;
52
+ });
@@ -0,0 +1,60 @@
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.getCachedTarball = getCachedTarball;
7
+ exports.putCachedTarball = putCachedTarball;
8
+ exports.getCachedIndex = getCachedIndex;
9
+ exports.putCachedIndex = putCachedIndex;
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const fs_extra_1 = __importDefault(require("fs-extra"));
13
+ async function getCachedTarball(scope, name, version) {
14
+ const tarballPath = getTarballPath(scope, name, version);
15
+ if (!(await fs_extra_1.default.pathExists(tarballPath))) {
16
+ return null;
17
+ }
18
+ return fs_extra_1.default.readFile(tarballPath);
19
+ }
20
+ async function putCachedTarball(scope, name, version, tarball, integrity) {
21
+ const dir = getSkillVersionCacheDir(scope, name, version);
22
+ await fs_extra_1.default.ensureDir(dir);
23
+ await fs_extra_1.default.writeFile(path_1.default.join(dir, "SKILL.tar.gz"), tarball);
24
+ await fs_extra_1.default.writeFile(path_1.default.join(dir, "integrity.txt"), `${integrity}\n`, "utf8");
25
+ }
26
+ async function getCachedIndex(key, ttlMs) {
27
+ const indexPath = getIndexPath(key);
28
+ if (!(await fs_extra_1.default.pathExists(indexPath))) {
29
+ return null;
30
+ }
31
+ const stat = await fs_extra_1.default.stat(indexPath);
32
+ if (Date.now() - stat.mtimeMs > ttlMs) {
33
+ return null;
34
+ }
35
+ return fs_extra_1.default.readJson(indexPath);
36
+ }
37
+ async function putCachedIndex(key, value) {
38
+ const indexPath = getIndexPath(key);
39
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(indexPath));
40
+ await fs_extra_1.default.writeJson(indexPath, value, { spaces: 2 });
41
+ }
42
+ function getTarballPath(scope, name, version) {
43
+ return path_1.default.join(getSkillVersionCacheDir(scope, name, version), "SKILL.tar.gz");
44
+ }
45
+ function getSkillVersionCacheDir(scope, name, version) {
46
+ return path_1.default.join(getCacheRoot(), "skills", formatSkillCacheKey(scope, name), version);
47
+ }
48
+ function getIndexPath(key) {
49
+ const fileName = key === "root" ? "root.json" : `${sanitizeCacheKey(key)}.json`;
50
+ return path_1.default.join(getCacheRoot(), "index", fileName);
51
+ }
52
+ function getCacheRoot() {
53
+ return path_1.default.join(process.env.HOME ?? os_1.default.homedir(), ".cicada", "cache");
54
+ }
55
+ function formatSkillCacheKey(scope, name) {
56
+ return `${scope}__${name}`;
57
+ }
58
+ function sanitizeCacheKey(key) {
59
+ return key.replace(/[\\/]/g, "__");
60
+ }
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.fetchRootIndex = fetchRootIndex;
40
+ exports.fetchSkillIndex = fetchSkillIndex;
41
+ exports.fetchSkillTarball = fetchSkillTarball;
42
+ exports.extractSkillTarball = extractSkillTarball;
43
+ exports.getSkillTarballUrl = getSkillTarballUrl;
44
+ exports.computeIntegrity = computeIntegrity;
45
+ const crypto_1 = __importDefault(require("crypto"));
46
+ const fs_extra_1 = __importDefault(require("fs-extra"));
47
+ const os_1 = __importDefault(require("os"));
48
+ const path_1 = __importDefault(require("path"));
49
+ const tar = __importStar(require("tar"));
50
+ const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/cicada-skills/registry/main";
51
+ async function fetchRootIndex() {
52
+ return fetchJson(joinRegistryUrl("index.json"));
53
+ }
54
+ async function fetchSkillIndex(scope, name) {
55
+ return fetchJson(joinRegistryUrl("skills", scope, name, "index.json"));
56
+ }
57
+ async function fetchSkillTarball(scope, name, version) {
58
+ const tarballUrl = getSkillTarballUrl(scope, name, version);
59
+ const integrityUrl = joinRegistryUrl("skills", scope, name, "versions", version, "integrity.txt");
60
+ const [tarball, integrityText] = await Promise.all([
61
+ fetchBuffer(tarballUrl),
62
+ fetchText(integrityUrl)
63
+ ]);
64
+ const integrity = integrityText.trim();
65
+ const computed = computeIntegrity(tarball);
66
+ if (computed !== integrity) {
67
+ throw new Error(`Integrity mismatch for ${scope}/${name}@${version}: expected ${integrity}, got ${computed}`);
68
+ }
69
+ return { tarball, integrity };
70
+ }
71
+ async function extractSkillTarball(tarball, destinationDir) {
72
+ await fs_extra_1.default.ensureDir(destinationDir);
73
+ const tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "cicada-tarball-"));
74
+ const tarballPath = path_1.default.join(tempDir, "SKILL.tar.gz");
75
+ try {
76
+ await fs_extra_1.default.writeFile(tarballPath, tarball);
77
+ const entryPaths = [];
78
+ await tar.t({
79
+ file: tarballPath,
80
+ onentry: (entry) => {
81
+ entryPaths.push(entry.path);
82
+ entry.resume();
83
+ }
84
+ });
85
+ await tar.x({
86
+ file: tarballPath,
87
+ cwd: destinationDir,
88
+ strip: shouldStripLeadingDirectory(entryPaths) ? 1 : 0
89
+ });
90
+ }
91
+ finally {
92
+ await fs_extra_1.default.remove(tempDir);
93
+ }
94
+ }
95
+ function getSkillTarballUrl(scope, name, version) {
96
+ return joinRegistryUrl("skills", scope, name, "versions", version, "SKILL.tar.gz");
97
+ }
98
+ function computeIntegrity(value) {
99
+ return `sha256-${crypto_1.default.createHash("sha256").update(value).digest("base64")}`;
100
+ }
101
+ function getRegistryBaseUrl() {
102
+ return (process.env.CICADA_REGISTRY_URL ?? DEFAULT_REGISTRY_URL).replace(/\/+$/, "");
103
+ }
104
+ function joinRegistryUrl(...segments) {
105
+ return `${getRegistryBaseUrl()}/${segments.map((segment) => segment.replace(/^\/+|\/+$/g, "")).join("/")}`;
106
+ }
107
+ async function fetchJson(url) {
108
+ const response = await fetch(url, { headers: { connection: "close" } });
109
+ if (!response.ok) {
110
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
111
+ }
112
+ return (await response.json());
113
+ }
114
+ async function fetchText(url) {
115
+ const response = await fetch(url, { headers: { connection: "close" } });
116
+ if (!response.ok) {
117
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
118
+ }
119
+ return response.text();
120
+ }
121
+ async function fetchBuffer(url) {
122
+ const response = await fetch(url, { headers: { connection: "close" } });
123
+ if (!response.ok) {
124
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
125
+ }
126
+ return Buffer.from(await response.arrayBuffer());
127
+ }
128
+ function shouldStripLeadingDirectory(entryPaths) {
129
+ const files = entryPaths.filter((entryPath) => !entryPath.endsWith("/"));
130
+ if (files.length === 0 || files.some((entryPath) => entryPath === "SKILL.md")) {
131
+ return false;
132
+ }
133
+ const [firstSegment] = files[0].split("/");
134
+ return Boolean(firstSegment) && files.every((entryPath) => entryPath.startsWith(`${firstSegment}/`));
135
+ }
@@ -0,0 +1,29 @@
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.VersionNotFoundError = void 0;
7
+ exports.resolveVersion = resolveVersion;
8
+ const semver_1 = __importDefault(require("semver"));
9
+ class VersionNotFoundError extends Error {
10
+ constructor(spec, availableVersions) {
11
+ super(`No version found for range "${spec}" (available: ${availableVersions.join(", ") || "none"})`);
12
+ this.name = "VersionNotFoundError";
13
+ }
14
+ }
15
+ exports.VersionNotFoundError = VersionNotFoundError;
16
+ function resolveVersion(spec, availableVersions) {
17
+ const versions = availableVersions.filter((version) => semver_1.default.valid(version)).sort(semver_1.default.rcompare);
18
+ if (versions.length === 0) {
19
+ throw new VersionNotFoundError(spec, availableVersions);
20
+ }
21
+ if (spec === "latest") {
22
+ return versions[0];
23
+ }
24
+ const max = semver_1.default.maxSatisfying(versions, spec);
25
+ if (!max) {
26
+ throw new VersionNotFoundError(spec, availableVersions);
27
+ }
28
+ return max;
29
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });