Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions packages/config-yaml/src/load/unroll.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PackageIdentifier } from "../interfaces/slugs.js";
import type { Registry } from "../interfaces/index.js";
import {
fillTemplateVariables,
getTemplateVariables,
parseMarkdownRuleOrAssistantUnrolled,
unrollBlocks,
replaceInputsWithSecrets,
} from "./unroll.js";

Expand Down Expand Up @@ -94,6 +96,133 @@ model: # should be models
});
});

describe("unrollBlocks markdown file blocks", () => {
it("unrolls markdown prompt file blocks into prompts", async () => {
const registry: Registry = {
async getContent(fullSlug) {
if (
fullSlug.uriType === "file" &&
fullSlug.fileUri === "prompts/scotty.md"
) {
return `
---
name: Scotty
description: Engineering prompt
---
Use concise engineering language.
`;
}

throw new Error("Unexpected registry lookup");
},
};

const result = await unrollBlocks(
{
name: "Test Assistant",
version: "1.0.0",
prompts: [{ uses: "file://prompts/scotty.md" }],
},
registry,
undefined,
);

expect(result.config).toBeDefined();
expect(result.config!.prompts).toEqual([
{
name: "Scotty",
description: "Engineering prompt",
prompt: "Use concise engineering language.",
sourceFile: "prompts/scotty.md",
},
]);
expect(result.config!.rules).toBeUndefined();
});

it("uses markdown prompt filenames when frontmatter omits name", async () => {
const registry: Registry = {
async getContent(fullSlug) {
if (
fullSlug.uriType === "file" &&
fullSlug.fileUri === "prompts/fallback.md"
) {
return `
---
description: Fallback prompt
---
Use the fallback filename.
`;
}

throw new Error("Unexpected registry lookup");
},
};

const result = await unrollBlocks(
{
name: "Test Assistant",
version: "1.0.0",
prompts: [{ uses: "file://prompts/fallback.md" }],
},
registry,
undefined,
);

expect(result.config?.prompts?.[0]).toEqual({
name: "fallback",
description: "Fallback prompt",
prompt: "Use the fallback filename.",
sourceFile: "prompts/fallback.md",
});
});

it("keeps markdown rule blocks resolving as rules", async () => {
const registry: Registry = {
async getContent(fullSlug) {
if (
fullSlug.uriType === "file" &&
fullSlug.fileUri === "rules/concise.md"
) {
return `
---
name: concise-rule
description: Keep replies concise
---
Respond in short paragraphs.
`;
}

throw new Error("Unexpected registry lookup");
},
};

const result = await unrollBlocks(
{
name: "Test Assistant",
version: "1.0.0",
rules: [{ uses: "file://rules/concise.md" }],
},
registry,
undefined,
);

expect(result.config).toBeDefined();
expect(result.config!.rules).toEqual([
{
name: "concise-rule",
description: "Keep replies concise",
globs: undefined,
regex: undefined,
alwaysApply: undefined,
invokable: undefined,
rule: "Respond in short paragraphs.",
sourceFile: "rules/concise.md",
},
]);
expect(result.config!.prompts).toBeUndefined();
});
});

describe("replaceInputsWithSecrets tests", () => {
it("replaces single input with secret", () => {
const yamlContent = `
Expand Down
29 changes: 26 additions & 3 deletions packages/config-yaml/src/load/unroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
PackageSlug,
packageSlugsEqual,
} from "../interfaces/slugs.js";
import { markdownToRule } from "../markdown/index.js";
import { markdownToRule, parseMarkdownPrompt } from "../markdown/index.js";
import {
AssistantUnrolled,
assistantUnrolledSchema,
Expand Down Expand Up @@ -437,6 +437,7 @@ export async function unrollBlocks(
blockIdentifier,
unrolledBlock.with,
registry,
section,
);
const block = blockConfigYaml[section]?.[0];
if (block) {
Expand Down Expand Up @@ -720,6 +721,7 @@ export async function resolveBlock(
id: PackageIdentifier,
inputs: Record<string, string | undefined> | undefined,
registry: Registry,
sectionHint?: keyof AssistantUnrolled,
): Promise<AssistantUnrolled> {
// Retrieve block raw yaml
const rawYaml = await registry.getContent(id);
Expand Down Expand Up @@ -753,7 +755,11 @@ export async function resolveBlock(
}

// Add source slug for mcp servers
const parsed = parseMarkdownRuleOrAssistantUnrolled(templatedYaml, id);
const parsed = parseMarkdownRuleOrAssistantUnrolled(
templatedYaml,
id,
sectionHint,
);
if (
id.uriType === "slug" &&
"mcpServers" in parsed &&
Expand All @@ -768,8 +774,14 @@ export async function resolveBlock(
export function parseMarkdownRuleOrAssistantUnrolled(
content: string,
id: PackageIdentifier,
sectionHint?: keyof AssistantUnrolled,
): AssistantUnrolled {
return parseYamlOrMarkdownRule<AssistantUnrolled>(content, id, parseBlock);
return parseYamlOrMarkdownRule<AssistantUnrolled>(
content,
id,
parseBlock,
sectionHint,
);
}

function parseMarkdownRuleOrConfigYaml(
Expand All @@ -783,6 +795,7 @@ function parseYamlOrMarkdownRule<T>(
content: string,
id: PackageIdentifier,
parseYamlFn: (content: string) => T,
sectionHint?: keyof AssistantUnrolled,
): T {
let parsedYaml: T;
try {
Expand All @@ -797,6 +810,16 @@ function parseYamlOrMarkdownRule<T>(
}
// If YAML parsing fails, try parsing as markdown rule
try {
if (sectionHint === "prompts") {
const prompt = parseMarkdownPrompt(content, id);
parsedYaml = {
name: prompt.name,
version: "1.0.0",
prompts: [prompt],
} as T;
return parsedYaml;
}

const rule = markdownToRule(content, id);
// Convert the rule object to the expected format
parsedYaml = { name: rule.name, version: "1.0.0", rules: [rule] } as T;
Expand Down
1 change: 1 addition & 0 deletions packages/config-yaml/src/markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./createMarkdownPrompt.js";
export * from "./createMarkdownRule.js";
export * from "./getRuleType.js";
export * from "./markdownToRule.js";
export * from "./parseMarkdownPrompt.js";
export * from "./agentFiles.js";
37 changes: 37 additions & 0 deletions packages/config-yaml/src/markdown/parseMarkdownPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
PackageIdentifier,
packageIdentifierToDisplayName,
} from "../browser.js";
import { Prompt } from "../schemas/index.js";
import { parseMarkdownRule, RuleFrontmatter } from "./markdownToRule.js";

function getPromptName(
frontmatter: RuleFrontmatter,
id: PackageIdentifier,
): string {
if (frontmatter.name) {
return frontmatter.name;
}

if (id.uriType === "file") {
const segments = id.fileUri.split(/[/\\]/);
const basename = segments.at(-1) || id.fileUri;
return basename.replace(/\.md$/i, "");
}

return packageIdentifierToDisplayName(id);
}

export function parseMarkdownPrompt(
content: string,
id: PackageIdentifier,
): Prompt {
const { frontmatter, markdown } = parseMarkdownRule(content);

return {
name: getPromptName(frontmatter, id),
description: frontmatter.description,
prompt: markdown,
sourceFile: id.uriType === "file" ? id.fileUri : undefined,
};
}
Loading