dotagent
Version:
Multi-file AI agent configuration manager with .agent directory support
1,354 lines (1,334 loc) • 46.9 kB
JavaScript
// src/parser.ts
import { unified } from "unified";
import remarkParse from "remark-parse";
import { toMarkdown } from "mdast-util-to-markdown";
import yaml from "js-yaml";
function parseAgentMarkdown(markdown, options = {}) {
console.warn("Warning: parseAgentMarkdown() is deprecated. Use importAgent() to import from .agent/ directory instead.");
const processor = unified().use(remarkParse);
const tree = processor.parse(markdown);
const rules = [];
let currentMetadata = null;
let currentContent = [];
let currentPosition;
for (let i = 0; i < tree.children.length; i++) {
const node = tree.children[i];
if (node.type === "html" && isRuleComment(node.value)) {
if (currentMetadata && currentContent.length > 0) {
rules.push({
metadata: currentMetadata,
content: nodesToMarkdown(currentContent),
position: currentPosition
});
}
currentMetadata = parseRuleComment(node.value);
currentContent = [];
currentPosition = node.position ? {
start: { ...node.position.start },
end: { ...node.position.end }
} : void 0;
} else if (currentMetadata) {
currentContent.push(node);
if (currentPosition && node.position) {
currentPosition.end = { ...node.position.end };
}
}
}
if (currentMetadata && currentContent.length > 0) {
rules.push({
metadata: currentMetadata,
content: nodesToMarkdown(currentContent),
position: currentPosition
});
}
return rules;
}
function isRuleComment(html) {
return /<!--\s*@[a-zA-Z0-9-]+(\s|$)/.test(html);
}
function parseRuleComment(html) {
const match = html.match(/<!--\s*@([a-zA-Z0-9-]+)\s*([\s\S]*?)\s*-->/);
if (!match) {
throw new Error("Invalid rule comment format");
}
const id = match[1];
const metaContent = match[2].trim();
const metadata = { id };
if (!metaContent) {
return metadata;
}
if (metaContent.includes("\n") || metaContent.startsWith("-") || metaContent.includes(": ")) {
try {
const parsed = yaml.load(metaContent);
if (typeof parsed === "object" && parsed !== null) {
return { ...parsed, id };
}
} catch {
}
}
if (!metaContent.includes("\n")) {
const pairs = metaContent.matchAll(/(\w+):(\S+)(?:\s|$)/g);
for (const [, key, value] of pairs) {
if (key === "scope" && value.includes(",")) {
metadata[key] = value.split(",").map((s) => s.trim());
} else if (key === "alwaysApply" || key === "manual") {
metadata[key] = value === "true";
} else if (key !== "id") {
metadata[key] = value;
}
}
} else {
const lines = metaContent.split("\n");
for (const line of lines) {
const colonIndex = line.indexOf(":");
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
if (key === "scope" && value.includes(",")) {
metadata[key] = value.split(",").map((s) => s.trim());
} else if (key === "alwaysApply" || key === "manual") {
metadata[key] = value === "true";
} else if (key !== "id" && value) {
metadata[key] = value;
}
}
}
}
return metadata;
}
function nodesToMarkdown(nodes) {
const tree = {
type: "root",
children: nodes
};
return toMarkdown(tree, {
bullet: "-",
emphasis: "*",
rule: "-"
}).trim();
}
function parseFenceEncodedMarkdown(markdown, options = {}) {
const processor = unified().use(remarkParse);
const tree = processor.parse(markdown);
const rules = [];
let currentMetadata = null;
let currentContent = [];
let currentPosition;
for (let i = 0; i < tree.children.length; i++) {
const node = tree.children[i];
if (node.type === "code" && node.lang === "rule") {
if (currentMetadata && currentContent.length > 0) {
rules.push({
metadata: currentMetadata,
content: nodesToMarkdown(currentContent),
position: currentPosition
});
}
try {
currentMetadata = yaml.load(node.value);
if (!currentMetadata.id) {
currentMetadata.id = `rule-${Date.now()}`;
}
currentContent = [];
currentPosition = node.position ? {
start: { ...node.position.start },
end: { ...node.position.end }
} : void 0;
} catch (e) {
if (options.strict) {
throw new Error(`Failed to parse rule metadata: ${e}`);
}
currentMetadata = null;
}
} else if (currentMetadata) {
currentContent.push(node);
if (currentPosition && node.position) {
currentPosition.end = { ...node.position.end };
}
}
}
if (currentMetadata && currentContent.length > 0) {
rules.push({
metadata: currentMetadata,
content: nodesToMarkdown(currentContent),
position: currentPosition
});
}
return rules;
}
// src/importers.ts
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
import { join, basename } from "path";
import matter from "gray-matter";
// src/yaml-parser.ts
import yaml2 from "js-yaml";
function createSafeYamlParser() {
return {
parse: (str) => {
const processedStr = str.replace(
/^(\s*\w+:\s*)(\*[^\n\r"']*?)(\s*(?:\r?\n|$))/gm,
(match, prefix, value, suffix) => {
if (value.startsWith('"') || value.startsWith("'")) {
return match;
}
return `${prefix}"${value}"${suffix}`;
}
);
const fullyProcessedStr = processedStr.replace(
/^(\s*-\s+)(\*[^\n\r"']*?)(\s*(?:\r?\n|$))/gm,
(match, prefix, value, suffix) => {
if (value.startsWith('"') || value.startsWith("'")) {
return match;
}
return `${prefix}"${value}"${suffix}`;
}
);
try {
return yaml2.load(fullyProcessedStr);
} catch (error) {
return yaml2.load(str);
}
},
stringify: (data) => {
const yamlOutput = yaml2.dump(data);
const lines = yamlOutput.split(/\r?\n/);
const out = [];
let inGlobsArray = false;
let globsIndent = "";
const containsGlob = (s) => s.includes("*");
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
const globsMatch = line.match(/^(\s*)globs:\s*(.*)$/);
if (globsMatch) {
globsIndent = globsMatch[1];
const value = globsMatch[2];
if (value === "") {
inGlobsArray = true;
out.push(line);
continue;
}
const scalar = value.match(/^(['"])(.+)\1(\s*(?:#.*)?)$/);
if (scalar && containsGlob(scalar[2])) {
line = `${globsIndent}globs: ${scalar[2]}${scalar[3] ?? ""}`;
}
out.push(line);
continue;
}
if (inGlobsArray) {
if (!line.startsWith(globsIndent + " ")) {
inGlobsArray = false;
i--;
continue;
}
const item = line.match(/^(\s*-\s*)(['"])(.+)\2(\s*(?:#.*)?)$/);
if (item && containsGlob(item[3])) {
line = `${item[1]}${item[3]}${item[4] ?? ""}`;
}
out.push(line);
continue;
}
out.push(line);
}
return out.join("\n");
}
};
}
var grayMatterOptions = {
engines: {
yaml: createSafeYamlParser()
}
};
// src/importers.ts
function isPrivateRule(filePath) {
const lowerPath = filePath.toLowerCase();
return lowerPath.includes(".local.") || lowerPath.includes("/private/") || lowerPath.includes("\\private\\");
}
async function importAll(repoPath) {
const results = [];
const errors = [];
const agentDir = join(repoPath, ".agent");
if (existsSync(agentDir)) {
try {
results.push(importAgent(agentDir));
} catch (e) {
errors.push({ file: agentDir, error: String(e) });
}
}
const copilotPath = join(repoPath, ".github", "copilot-instructions.md");
if (existsSync(copilotPath)) {
try {
results.push(importCopilot(copilotPath));
} catch (e) {
errors.push({ file: copilotPath, error: String(e) });
}
}
const copilotLocalPath = join(repoPath, ".github", "copilot-instructions.local.md");
if (existsSync(copilotLocalPath)) {
try {
results.push(importCopilot(copilotLocalPath));
} catch (e) {
errors.push({ file: copilotLocalPath, error: String(e) });
}
}
const cursorDir = join(repoPath, ".cursor");
if (existsSync(cursorDir)) {
try {
results.push(importCursor(cursorDir));
} catch (e) {
errors.push({ file: cursorDir, error: String(e) });
}
}
const cursorRulesFile = join(repoPath, ".cursorrules");
if (existsSync(cursorRulesFile)) {
try {
results.push(importCursorLegacy(cursorRulesFile));
} catch (e) {
errors.push({ file: cursorRulesFile, error: String(e) });
}
}
const clinerules = join(repoPath, ".clinerules");
if (existsSync(clinerules)) {
try {
results.push(importCline(clinerules));
} catch (e) {
errors.push({ file: clinerules, error: String(e) });
}
}
const clinerulesLocal = join(repoPath, ".clinerules.local");
if (existsSync(clinerulesLocal)) {
try {
results.push(importCline(clinerulesLocal));
} catch (e) {
errors.push({ file: clinerulesLocal, error: String(e) });
}
}
const windsurfRules = join(repoPath, ".windsurfrules");
if (existsSync(windsurfRules)) {
try {
results.push(importWindsurf(windsurfRules));
} catch (e) {
errors.push({ file: windsurfRules, error: String(e) });
}
}
const windsurfRulesLocal = join(repoPath, ".windsurfrules.local");
if (existsSync(windsurfRulesLocal)) {
try {
results.push(importWindsurf(windsurfRulesLocal));
} catch (e) {
errors.push({ file: windsurfRulesLocal, error: String(e) });
}
}
const zedRules = join(repoPath, ".rules");
if (existsSync(zedRules)) {
try {
results.push(importZed(zedRules));
} catch (e) {
errors.push({ file: zedRules, error: String(e) });
}
}
const zedRulesLocal = join(repoPath, ".rules.local");
if (existsSync(zedRulesLocal)) {
try {
results.push(importZed(zedRulesLocal));
} catch (e) {
errors.push({ file: zedRulesLocal, error: String(e) });
}
}
const agentsMd = join(repoPath, "AGENTS.md");
if (existsSync(agentsMd)) {
try {
results.push(importCodex(agentsMd));
} catch (e) {
errors.push({ file: agentsMd, error: String(e) });
}
}
const agentsLocalMd = join(repoPath, "AGENTS.local.md");
if (existsSync(agentsLocalMd)) {
try {
results.push(importCodex(agentsLocalMd));
} catch (e) {
errors.push({ file: agentsLocalMd, error: String(e) });
}
}
const claudeMd = join(repoPath, "CLAUDE.md");
if (existsSync(claudeMd)) {
try {
results.push(importClaudeCode(claudeMd));
} catch (e) {
errors.push({ file: claudeMd, error: String(e) });
}
}
const opencodeMd = join(repoPath, "AGENTS.md");
if (existsSync(opencodeMd)) {
try {
results.push(importOpenCode(opencodeMd));
} catch (e) {
errors.push({ file: opencodeMd, error: String(e) });
}
}
const geminiMd = join(repoPath, "GEMINI.md");
if (existsSync(geminiMd)) {
try {
results.push(importGemini(geminiMd));
} catch (e) {
errors.push({ file: geminiMd, error: String(e) });
}
}
const bestPracticesMd = join(repoPath, "best_practices.md");
if (existsSync(bestPracticesMd)) {
try {
results.push(importQodo(bestPracticesMd));
} catch (e) {
errors.push({ file: bestPracticesMd, error: String(e) });
}
}
const claudeLocalMd = join(repoPath, "CLAUDE.local.md");
if (existsSync(claudeLocalMd)) {
try {
results.push(importClaudeCode(claudeLocalMd));
} catch (e) {
errors.push({ file: claudeLocalMd, error: String(e) });
}
}
const geminiLocalMd = join(repoPath, "GEMINI.local.md");
if (existsSync(geminiLocalMd)) {
try {
results.push(importGemini(geminiLocalMd));
} catch (e) {
errors.push({ file: geminiLocalMd, error: String(e) });
}
}
const conventionsMd = join(repoPath, "CONVENTIONS.md");
if (existsSync(conventionsMd)) {
try {
results.push(importAider(conventionsMd));
} catch (e) {
errors.push({ file: conventionsMd, error: String(e) });
}
}
const conventionsLocalMd = join(repoPath, "CONVENTIONS.local.md");
if (existsSync(conventionsLocalMd)) {
try {
results.push(importAider(conventionsLocalMd));
} catch (e) {
errors.push({ file: conventionsLocalMd, error: String(e) });
}
}
const amazonqRulesDir = join(repoPath, ".amazonq", "rules");
if (existsSync(amazonqRulesDir)) {
try {
results.push(importAmazonQ(amazonqRulesDir));
} catch (e) {
errors.push({ file: amazonqRulesDir, error: String(e) });
}
}
const rooRulesDir = join(repoPath, ".roo", "rules");
if (existsSync(rooRulesDir)) {
try {
results.push(importRoo(rooRulesDir));
} catch (e) {
errors.push({ file: rooRulesDir, error: String(e) });
}
}
const junieGuidelines = join(repoPath, ".junie", "guidelines.md");
if (existsSync(junieGuidelines)) {
try {
results.push(importJunie(junieGuidelines));
} catch (e) {
errors.push({ file: junieGuidelines, error: String(e) });
}
}
return { results, errors };
}
function importCopilot(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivate = isPrivateRule(filePath);
const metadata = {
id: "copilot-instructions",
alwaysApply: true,
description: "GitHub Copilot custom instructions"
};
if (isPrivate) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "copilot",
filePath,
rules,
raw: content
};
}
function importAgent(agentDir) {
const rules = [];
function findMarkdownFiles(dir, relativePath = "") {
const entries = readdirSync(dir, { withFileTypes: true });
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = relativePath ? join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
findMarkdownFiles(fullPath, relPath);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
const content = readFileSync(fullPath, "utf-8");
const { data, content: body } = matter(content, grayMatterOptions);
let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, ""));
if (segments[0] === "private") segments = segments.slice(1);
const defaultId = segments.join("/");
const isPrivateFile = isPrivateRule(fullPath);
const metadata = {
id: data.id || defaultId,
...data
};
if (metadata.alwaysApply === void 0) {
metadata.alwaysApply = false;
}
if (data.private === true || data.private === void 0 && isPrivateFile) {
metadata.private = true;
}
rules.push({
metadata,
content: body.trim()
});
}
}
}
findMarkdownFiles(agentDir);
return {
format: "agent",
filePath: agentDir,
rules
};
}
function importCursor(cursorDir) {
const rules = [];
function findCursorFiles(dir, relativePath = "") {
const entries = readdirSync(dir, { withFileTypes: true });
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = relativePath ? join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
findCursorFiles(fullPath, relPath);
} else if (entry.isFile() && (entry.name.endsWith(".mdc") || entry.name.endsWith(".md"))) {
const content = readFileSync(fullPath, "utf-8");
const { data, content: body } = matter(content, grayMatterOptions);
let segments = relPath.replace(/\.(mdc|md)$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, ""));
if (segments[0] === "private") segments = segments.slice(1);
if (segments[0] === "rules" && segments.length === 2) segments = segments.slice(1);
const defaultId = segments.join("/");
const isPrivateFile = isPrivateRule(fullPath);
const metadata = {
id: data.id || defaultId,
...data
};
if (metadata.alwaysApply === void 0) {
metadata.alwaysApply = false;
}
if (data.private === true || data.private === void 0 && isPrivateFile) {
metadata.private = true;
}
rules.push({
metadata,
content: body.trim()
});
}
}
}
findCursorFiles(cursorDir);
return {
format: "cursor",
filePath: cursorDir,
rules
};
}
function importCursorLegacy(filePath) {
const content = readFileSync(filePath, "utf-8");
const rules = [{
metadata: {
id: "cursor-rules-legacy",
alwaysApply: true,
description: "Legacy Cursor rules"
},
content: content.trim()
}];
return {
format: "cursor",
filePath,
rules,
raw: content
};
}
function importCline(rulesPath) {
const rules = [];
if (existsSync(rulesPath) && statSync(rulesPath).isDirectory()) {
let findMdFiles2 = function(dir, relativePath = "") {
const entries = readdirSync(dir, { withFileTypes: true });
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = relativePath ? join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
findMdFiles2(fullPath, relPath);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
const content = readFileSync(fullPath, "utf-8");
const isPrivateFile = isPrivateRule(fullPath);
let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, ""));
if (segments[0] === "private") segments = segments.slice(1);
const defaultId = segments.join("/");
const metadata = {
id: defaultId,
alwaysApply: true,
description: `Cline rules from ${relPath}`
};
if (isPrivateFile) {
metadata.private = true;
}
rules.push({
metadata,
content: content.trim()
});
}
}
};
var findMdFiles = findMdFiles2;
findMdFiles2(rulesPath);
} else {
const content = readFileSync(rulesPath, "utf-8");
const isPrivateFile = isPrivateRule(rulesPath);
const metadata = {
id: "cline-rules",
alwaysApply: true,
description: "Cline project rules"
};
if (isPrivateFile) {
metadata.private = true;
}
rules.push({
metadata,
content: content.trim()
});
}
return {
format: "cline",
filePath: rulesPath,
rules
};
}
function importWindsurf(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "windsurf-rules",
alwaysApply: true,
description: "Windsurf AI rules"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "windsurf",
filePath,
rules,
raw: content
};
}
function importZed(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "zed-rules",
alwaysApply: true,
description: "Zed editor rules"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "zed",
filePath,
rules,
raw: content
};
}
function importCodex(filePath) {
const content = readFileSync(filePath, "utf-8");
const format = basename(filePath) === "AGENTS.md" || basename(filePath) === "AGENTS.local.md" ? "codex" : "unknown";
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: format === "codex" ? "codex-agents" : "claude-rules",
alwaysApply: true,
description: format === "codex" ? "OpenAI Codex agent instructions" : "Claude AI instructions"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format,
filePath,
rules,
raw: content
};
}
function importAider(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "aider-conventions",
alwaysApply: true,
description: "Aider CLI conventions"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "aider",
filePath,
rules,
raw: content
};
}
function importClaudeCode(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "claude-code-instructions",
alwaysApply: true,
description: "Claude Code context and instructions"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "claude",
filePath,
rules,
raw: content
};
}
function importOpenCode(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "opencode-agents",
alwaysApply: true,
description: "OpenCode agents and instructions"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "opencode",
filePath,
rules,
raw: content
};
}
function importGemini(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "gemini-instructions",
alwaysApply: true,
description: "Gemini CLI context and instructions"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "gemini",
filePath,
rules,
raw: content
};
}
function importQodo(filePath) {
const content = readFileSync(filePath, "utf-8");
const rules = [{
metadata: {
id: "qodo-best-practices",
alwaysApply: true,
description: "Qodo best practices and coding standards",
scope: "**/*",
priority: "high"
},
content: content.trim()
}];
return {
format: "qodo",
filePath,
rules,
raw: content
};
}
function importAmazonQ(rulesDir) {
const rules = [];
function findMdFiles(dir, relativePath = "") {
const entries = readdirSync(dir, { withFileTypes: true });
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = relativePath ? join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
findMdFiles(fullPath, relPath);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
const content = readFileSync(fullPath, "utf-8");
const isPrivateFile = isPrivateRule(fullPath);
let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, ""));
if (segments[0] === "private") segments = segments.slice(1);
const defaultId = segments.join("/");
const metadata = {
id: `amazonq-${defaultId}`,
alwaysApply: true,
description: `Amazon Q rules from ${relPath}`
};
if (isPrivateFile) {
metadata.private = true;
}
rules.push({
metadata,
content: content.trim()
});
}
}
}
findMdFiles(rulesDir);
return {
format: "amazonq",
filePath: rulesDir,
rules
};
}
function importRoo(rulesDir) {
const rules = [];
function findMdFiles(dir, relativePath = "") {
const entries = readdirSync(dir, { withFileTypes: true });
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = relativePath ? join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
findMdFiles(fullPath, relPath);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
const content = readFileSync(fullPath, "utf-8");
const { data, content: body } = matter(content, grayMatterOptions);
let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, ""));
if (segments[0] === "private") segments = segments.slice(1);
const defaultId = segments.join("/");
const isPrivateFile = isPrivateRule(fullPath);
const metadata = {
id: data.id || defaultId,
...data
};
if (metadata.alwaysApply === void 0) {
metadata.alwaysApply = false;
}
if (data.private === true || data.private === void 0 && isPrivateFile) {
metadata.private = true;
}
rules.push({
metadata,
content: body.trim()
});
}
}
}
findMdFiles(rulesDir);
return {
format: "roo",
filePath: rulesDir,
rules
};
}
function importJunie(filePath) {
const content = readFileSync(filePath, "utf-8");
const isPrivateFile = isPrivateRule(filePath);
const metadata = {
id: "junie-guidelines",
alwaysApply: true,
description: "JetBrains Junie guidelines and instructions"
};
if (isPrivateFile) {
metadata.private = true;
}
const rules = [{
metadata,
content: content.trim()
}];
return {
format: "junie",
filePath,
rules,
raw: content
};
}
// src/exporters.ts
import { writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
import { join as join2, dirname } from "path";
import yaml3 from "js-yaml";
import matter2 from "gray-matter";
function generateConditionalRulesSection(rules, repoPath) {
const sections = [];
const alwaysApplyRules = rules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalRules = rules.filter((r) => r.metadata.alwaysApply === false);
if (conditionalRules.length === 0) {
return "";
}
const rulesByFolder = {};
const rulesWithScope = [];
const rulesWithDescription = [];
conditionalRules.forEach((rule) => {
if (rule.metadata.id && rule.metadata.id.includes("/")) {
const folder = rule.metadata.id.split("/")[0];
if (!rulesByFolder[folder]) {
rulesByFolder[folder] = [];
}
rulesByFolder[folder].push(rule);
}
if (rule.metadata.scope) {
rulesWithScope.push(rule);
} else if (rule.metadata.description && !rule.metadata.scope && !rule.metadata.id?.includes("/")) {
rulesWithDescription.push(rule);
}
});
sections.push("## Context-Specific Rules");
sections.push("");
if (rulesWithScope.length > 0) {
rulesWithScope.forEach((rule) => {
const scopes = Array.isArray(rule.metadata.scope) ? rule.metadata.scope : [rule.metadata.scope];
scopes.forEach((scope) => {
const rulePath = `.agent/${rule.metadata.id}.md`;
const description = rule.metadata.description ? ` - ${rule.metadata.description}` : "";
sections.push(`When working with files matching \`${scope}\`, also apply:`);
sections.push(`\u2192 [${rule.metadata.id}](${rulePath})${description}`);
sections.push("");
});
});
}
if (rulesWithDescription.length > 0) {
rulesWithDescription.forEach((rule) => {
const rulePath = `.agent/${rule.metadata.id}.md`;
sections.push(`When working with ${rule.metadata.description}, also apply:`);
sections.push(`\u2192 [${rule.metadata.id}](${rulePath})`);
sections.push("");
});
}
Object.entries(rulesByFolder).forEach(([folder, folderRules]) => {
const unhandledRules = folderRules.filter(
(r) => !rulesWithScope.includes(r) && !rulesWithDescription.includes(r)
);
if (unhandledRules.length > 0) {
const sectionTitle = folder.charAt(0).toUpperCase() + folder.slice(1);
sections.push(`## ${sectionTitle}`);
sections.push("");
unhandledRules.forEach((rule) => {
const rulePath = `.agent/${rule.metadata.id}.md`;
const description = rule.metadata.description ? ` - ${rule.metadata.description}` : "";
sections.push(`\u2192 [${rule.metadata.id}](${rulePath})${description}`);
});
sections.push("");
}
});
return sections.join("\n");
}
function toAgentMarkdown(rules) {
console.warn("Warning: toAgentMarkdown() is deprecated. Use exportToAgent() to export to .agent/ directory instead.");
const sections = [];
for (const rule of rules) {
const { metadata, content } = rule;
const { id, ...otherMetadata } = metadata;
let metaComment = `<!-- @${id}`;
if (Object.keys(otherMetadata).length > 0) {
const metaYaml = yaml3.dump(otherMetadata, {
flowLevel: 1,
lineWidth: -1
}).trim();
metaComment += `
${metaYaml}`;
}
metaComment += " -->";
sections.push(`${metaComment}
${content}`);
}
return sections.join("\n\n");
}
function exportToCopilot(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n---\n\n");
const fullContent = conditionalSection ? `${mainContent}
---
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
}
function exportToAgent(rules, outputDir, options) {
const agentDir = join2(outputDir, ".agent");
mkdirSync(agentDir, { recursive: true });
let topIndex = 1;
rules.forEach((rule) => {
let filename;
let filePath;
if (rule.metadata.id && rule.metadata.id.includes("/")) {
const parts = rule.metadata.id.split("/");
const fileName = parts.pop() + ".md";
const subDir = join2(agentDir, ...parts);
mkdirSync(subDir, { recursive: true });
filePath = join2(subDir, fileName);
} else {
if (rule.metadata.private) {
const prefix = String(topIndex).padStart(3, "0") + "-";
topIndex++;
filename = `${prefix}${rule.metadata.id || "rule"}.md`;
const privDir = join2(agentDir, "private");
mkdirSync(privDir, { recursive: true });
filePath = join2(privDir, filename);
} else {
filename = `${rule.metadata.id || "rule"}.md`;
filePath = join2(agentDir, filename);
}
}
const frontMatterBase = {};
if (rule.metadata.description !== void 0 && rule.metadata.description !== null) frontMatterBase.description = rule.metadata.description;
if (rule.metadata.alwaysApply !== void 0) frontMatterBase.alwaysApply = rule.metadata.alwaysApply;
if (rule.metadata.globs !== void 0 && rule.metadata.globs !== null) frontMatterBase.globs = rule.metadata.globs;
if (rule.metadata.manual !== void 0 && rule.metadata.manual !== null) frontMatterBase.manual = rule.metadata.manual;
if (rule.metadata.scope !== void 0 && rule.metadata.scope !== null) frontMatterBase.scope = rule.metadata.scope;
if (rule.metadata.priority !== void 0 && rule.metadata.priority !== null) frontMatterBase.priority = rule.metadata.priority;
if (rule.metadata.triggers !== void 0 && rule.metadata.triggers !== null) frontMatterBase.triggers = rule.metadata.triggers;
for (const [key, value] of Object.entries(rule.metadata)) {
if (!["id", "description", "alwaysApply", "globs", "manual", "scope", "priority", "triggers"].includes(key) && value !== void 0 && value !== null) {
if (key === "private" && value === false) continue;
frontMatterBase[key] = value;
}
}
const frontMatter = frontMatterBase;
const mdContent = matter2.stringify(rule.content, frontMatter, grayMatterOptions);
writeFileSync(filePath, mdContent, "utf-8");
});
}
function exportToCursor(rules, outputDir, options) {
const rulesDir = join2(outputDir, ".cursor", "rules");
mkdirSync(rulesDir, { recursive: true });
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
for (const rule of filteredRules) {
let filePath;
if (rule.metadata.id && rule.metadata.id.includes("/")) {
const parts = rule.metadata.id.split("/");
const fileName = parts.pop() + ".mdc";
const subDir = join2(rulesDir, ...parts);
mkdirSync(subDir, { recursive: true });
filePath = join2(subDir, fileName);
} else {
const filename = `${rule.metadata.id || "rule"}.mdc`;
filePath = join2(rulesDir, filename);
}
const frontMatterBase = {};
if (rule.metadata.description !== void 0 && rule.metadata.description !== null) frontMatterBase.description = rule.metadata.description;
if (rule.metadata.alwaysApply !== void 0) frontMatterBase.alwaysApply = rule.metadata.alwaysApply;
if (rule.metadata.globs !== void 0 && rule.metadata.globs !== null) frontMatterBase.globs = rule.metadata.globs;
if (rule.metadata.manual !== void 0 && rule.metadata.manual !== null) frontMatterBase.manual = rule.metadata.manual;
if (rule.metadata.scope !== void 0 && rule.metadata.scope !== null) frontMatterBase.scope = rule.metadata.scope;
if (rule.metadata.priority !== void 0 && rule.metadata.priority !== null) frontMatterBase.priority = rule.metadata.priority;
if (rule.metadata.triggers !== void 0 && rule.metadata.triggers !== null) frontMatterBase.triggers = rule.metadata.triggers;
for (const [key, value] of Object.entries(rule.metadata)) {
if (!["id", "description", "alwaysApply", "globs", "manual", "scope", "priority", "triggers"].includes(key) && value !== void 0 && value !== null) {
if (key === "private" && value === false) continue;
frontMatterBase[key] = value;
}
}
const frontMatter = frontMatterBase;
const mdcContent = matter2.stringify(rule.content, frontMatter, grayMatterOptions);
writeFileSync(filePath, mdcContent, "utf-8");
}
}
function exportToCline(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
if (outputPath.endsWith(".clinerules")) {
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => {
const header = rule.metadata.description ? `## ${rule.metadata.description}
` : "";
return header + rule.content;
}).join("\n\n");
const fullContent = conditionalSection ? `${mainContent}
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
} else {
const rulesDir = join2(outputPath, ".clinerules");
mkdirSync(rulesDir, { recursive: true });
filteredRules.forEach((rule, index) => {
const filename = `${String(index + 1).padStart(2, "0")}-${rule.metadata.id || "rule"}.md`;
const filePath = join2(rulesDir, filename);
writeFileSync(filePath, rule.content, "utf-8");
});
}
}
function exportToWindsurf(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n");
const fullContent = conditionalSection ? `${mainContent}
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
}
function exportToZed(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n");
const fullContent = conditionalSection ? `${mainContent}
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
}
function exportSingleFileWithHeaders(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => {
const header = rule.metadata.description ? `# ${rule.metadata.description}
` : "";
return header + rule.content;
}).join("\n\n");
const fullContent = conditionalSection ? `${mainContent}
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
}
function exportToCodex(rules, outputPath, options) {
exportSingleFileWithHeaders(rules, outputPath, options);
}
function exportToAider(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n");
const fullContent = conditionalSection ? `${mainContent}
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
}
function exportToClaudeCode(rules, outputPath, options) {
exportSingleFileWithHeaders(rules, outputPath, options);
}
function exportToOpenCode(rules, outputPath, options) {
exportSingleFileWithHeaders(rules, outputPath, options);
}
function exportToGemini(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const content = filteredRules.map((rule) => {
const header = rule.metadata.description ? `# ${rule.metadata.description}
` : "";
return header + rule.content;
}).join("\n\n");
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, content, "utf-8");
}
function exportToQodo(rules, outputPath, options) {
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath));
const mainContent = alwaysApplyRules.map((rule) => {
const header = rule.metadata.description ? `# ${rule.metadata.description}
` : "";
return header + rule.content;
}).join("\n\n---\n\n");
const fullContent = conditionalSection ? `${mainContent}
---
${conditionalSection}` : mainContent;
ensureDirectoryExists(outputPath);
writeFileSync(outputPath, fullContent, "utf-8");
}
function exportToAmazonQ(rules, outputDir, options) {
const rulesDir = join2(outputDir, ".amazonq", "rules");
mkdirSync(rulesDir, { recursive: true });
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
for (const rule of filteredRules) {
let filePath;
if (rule.metadata.id && rule.metadata.id.includes("/")) {
const parts = rule.metadata.id.split("/");
const fileName = parts.pop() + ".md";
const subDir = join2(rulesDir, ...parts);
mkdirSync(subDir, { recursive: true });
filePath = join2(subDir, fileName);
} else {
const cleanId = rule.metadata.id?.startsWith("amazonq-") ? rule.metadata.id.substring(8) : rule.metadata.id || "rule";
const filename = `${cleanId}.md`;
filePath = join2(rulesDir, filename);
}
writeFileSync(filePath, rule.content, "utf-8");
}
}
function exportToRoo(rules, outputDir, options) {
const rulesDir = join2(outputDir, ".roo", "rules");
mkdirSync(rulesDir, { recursive: true });
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
for (const rule of filteredRules) {
let filePath;
if (rule.metadata.id && rule.metadata.id.includes("/")) {
const parts = rule.metadata.id.split("/");
const fileName = parts.pop() + ".md";
const subDir = join2(rulesDir, ...parts);
mkdirSync(subDir, { recursive: true });
filePath = join2(subDir, fileName);
} else {
const filename = `${rule.metadata.id || "rule"}.md`;
filePath = join2(rulesDir, filename);
}
const frontMatterBase = {};
if (rule.metadata.alwaysApply !== void 0) frontMatterBase.alwaysApply = rule.metadata.alwaysApply;
if (rule.metadata.description !== void 0 && rule.metadata.description !== null) frontMatterBase.description = rule.metadata.description;
if (rule.metadata.scope !== void 0 && rule.metadata.scope !== null) frontMatterBase.scope = rule.metadata.scope;
if (rule.metadata.globs !== void 0 && rule.metadata.globs !== null) frontMatterBase.globs = rule.metadata.globs;
if (rule.metadata.manual !== void 0 && rule.metadata.manual !== null) frontMatterBase.manual = rule.metadata.manual;
if (rule.metadata.priority !== void 0 && rule.metadata.priority !== null) frontMatterBase.priority = rule.metadata.priority;
if (rule.metadata.triggers !== void 0 && rule.metadata.triggers !== null) frontMatterBase.triggers = rule.metadata.triggers;
for (const [key, value] of Object.entries(rule.metadata)) {
if (!["id", "alwaysApply", "description", "scope", "globs", "manual", "priority", "triggers"].includes(key) && value !== void 0 && value !== null) {
if (key === "private" && value === false) continue;
frontMatterBase[key] = value;
}
}
const frontMatter = frontMatterBase;
const content = rule.content.startsWith("\n") ? rule.content : "\n" + rule.content;
const mdContent = matter2.stringify(content, frontMatter, grayMatterOptions);
writeFileSync(filePath, mdContent, "utf-8");
}
}
function exportToJunie(rules, outputDir, options) {
const junieDir = join2(outputDir, ".junie");
mkdirSync(junieDir, { recursive: true });
const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate);
const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false);
const conditionalSection = generateConditionalRulesSection(filteredRules, outputDir);
const mainContent = alwaysApplyRules.map((rule) => {
const header = rule.metadata.description ? `## ${rule.metadata.description}
` : "";
return header + rule.content;
}).join("\n\n");
const fullContent = conditionalSection ? `${mainContent}
${conditionalSection}` : mainContent;
const filePath = join2(junieDir, "guidelines.md");
writeFileSync(filePath, fullContent, "utf-8");
}
function exportAll(rules, repoPath, dryRun = false, options = { includePrivate: false }) {
if (!dryRun) {
exportToAgent(rules, repoPath, options);
exportToCopilot(rules, join2(repoPath, ".github", "copilot-instructions.md"), options);
exportToCursor(rules, repoPath, options);
exportToCline(rules, join2(repoPath, ".clinerules"), options);
exportToWindsurf(rules, join2(repoPath, ".windsurfrules"), options);
exportToZed(rules, join2(repoPath, ".rules"), options);
exportToCodex(rules, join2(repoPath, "AGENTS.md"), options);
exportToAider(rules, join2(repoPath, "CONVENTIONS.md"), options);
exportToClaudeCode(rules, join2(repoPath, "CLAUDE.md"), options);
exportToOpenCode(rules, join2(repoPath, "AGENTS.md"), options);
exportToGemini(rules, join2(repoPath, "GEMINI.md"), options);
exportToQodo(rules, join2(repoPath, "best_practices.md"), options);
exportToAmazonQ(rules, repoPath, options);
exportToRoo(rules, repoPath, options);
exportToJunie(rules, repoPath, options);
}
}
function ensureDirectoryExists(filePath) {
const dir = dirname(filePath);
if (!existsSync2(dir)) {
mkdirSync(dir, { recursive: true });
}
}
export {
exportAll,
exportToAgent,
exportToAider,
exportToAmazonQ,
exportToClaudeCode,
exportToCline,
exportToCodex,
exportToCopilot,
exportToCursor,
exportToGemini,
exportToJunie,
exportToOpenCode,
exportToQodo,
exportToRoo,
exportToWindsurf,
exportToZed,
importAgent,
importAider,
importAll,
importAmazonQ,
importClaudeCode,
importCline,
importCodex,
importCopilot,
importCursor,
importCursorLegacy,
importGemini,
importJunie,
importOpenCode,
importQodo,
importRoo,
importWindsurf,
importZed,
parseAgentMarkdown,
parseFenceEncodedMarkdown,
toAgentMarkdown
};
//# sourceMappingURL=index.js.map