mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 08:04:25 +00:00
feat: share cli ai guidance generation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
05b5898132
commit
5a48886b4c
@@ -1,10 +1,9 @@
|
||||
import { existsSync } from "fs";
|
||||
import { cp, mkdtemp, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import { mkdtemp, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import { tmpdir } from "os";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { generateAgentsMdContent } from "../../../cli/src/guidance/core.ts";
|
||||
import { SKILLS } from "../../../cli/src/guidance/skills.ts";
|
||||
import { writeAiGuidanceFiles } from "../../../cli/src/guidance/writer.ts";
|
||||
import {
|
||||
runPromptAndCapture,
|
||||
type PromptRunResult,
|
||||
@@ -86,7 +85,7 @@ export async function runCliArtifactEvalCase(
|
||||
variant: CliVariant;
|
||||
}
|
||||
): Promise<CliArtifactEvalResult> {
|
||||
const workspaceDir = await createIsolatedWorkspace(evalCase.id, options.variant.skillsSourcePath);
|
||||
const workspaceDir = await createIsolatedWorkspace(evalCase.id, options.variant);
|
||||
|
||||
try {
|
||||
const renderedPrompt = await renderPrompt(evalCase.prompt, workspaceDir);
|
||||
@@ -125,33 +124,23 @@ export function shouldKeepWorkspace(): boolean {
|
||||
|
||||
async function createIsolatedWorkspace(
|
||||
caseId: string,
|
||||
skillsSourcePath: string
|
||||
variant: CliVariant
|
||||
): Promise<string> {
|
||||
const workspaceDir = await mkdtemp(join(tmpdir(), `wmill-cli-artifact-${caseId}-`));
|
||||
const skillsDir = join(workspaceDir, ".claude", "skills");
|
||||
|
||||
await mkdir(dirname(skillsDir), { recursive: true });
|
||||
await cp(skillsSourcePath, skillsDir, { recursive: true });
|
||||
await writeProjectGuidance(workspaceDir);
|
||||
await mkdir(dirname(join(workspaceDir, ".claude", "skills")), { recursive: true });
|
||||
await writeAiGuidanceFiles({
|
||||
targetDir: workspaceDir,
|
||||
nonDottedPaths: true,
|
||||
overwriteProjectGuidance: true,
|
||||
skillsSourcePath: variant.skillsSourcePath,
|
||||
agentsSourcePath: variant.agentsSourcePath,
|
||||
claudeSourcePath: variant.claudeSourcePath,
|
||||
});
|
||||
await writeFile(join(workspaceDir, "rt.d.ts"), "export namespace RT {}\n", "utf8");
|
||||
|
||||
return workspaceDir;
|
||||
}
|
||||
|
||||
async function writeProjectGuidance(workspaceDir: string): Promise<void> {
|
||||
const skillsBaseDir = ".claude/skills";
|
||||
const skillsReference = SKILLS.map(
|
||||
(skill) => `- \`${skillsBaseDir}/${skill.name}/SKILL.md\` - ${skill.description}`
|
||||
).join("\n");
|
||||
|
||||
await writeFile(
|
||||
join(workspaceDir, "AGENTS.md"),
|
||||
generateAgentsMdContent(skillsReference),
|
||||
"utf8"
|
||||
);
|
||||
await writeFile(join(workspaceDir, "CLAUDE.md"), "Instructions are in @AGENTS.md\n", "utf8");
|
||||
}
|
||||
|
||||
async function renderPrompt(prompt: string, workspaceDir: string): Promise<string> {
|
||||
const renderedUserPrompt = prompt.replaceAll("{{workspace_root}}", workspaceDir);
|
||||
const agentsInstructions = await readFile(join(workspaceDir, "AGENTS.md"), "utf8");
|
||||
|
||||
@@ -16,12 +16,16 @@ interface CliVariantManifest {
|
||||
id: string;
|
||||
description?: string;
|
||||
skillsSource: CliVariantSource;
|
||||
agentsSourcePath?: string;
|
||||
claudeSourcePath?: string;
|
||||
}
|
||||
|
||||
export interface CliVariant {
|
||||
id: string;
|
||||
description?: string;
|
||||
skillsSourcePath: string;
|
||||
agentsSourcePath?: string;
|
||||
claudeSourcePath?: string;
|
||||
}
|
||||
|
||||
const VARIANTS_DIR = fileURLToPath(new URL("../../variants/cli", import.meta.url));
|
||||
@@ -45,7 +49,9 @@ export async function loadCliVariants(): Promise<CliVariant[]> {
|
||||
variants.push({
|
||||
id: parsed.id,
|
||||
description: parsed.description,
|
||||
skillsSourcePath: resolveVariantSkillsSource(parsed.skillsSource, manifestPath)
|
||||
skillsSourcePath: resolveVariantSkillsSource(parsed.skillsSource, manifestPath),
|
||||
agentsSourcePath: resolveOptionalManifestPath(parsed.agentsSourcePath, manifestPath),
|
||||
claudeSourcePath: resolveOptionalManifestPath(parsed.claudeSourcePath, manifestPath)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,3 +77,14 @@ function resolveVariantSkillsSource(
|
||||
|
||||
return resolve(join(manifestPath, ".."), skillsSource.path);
|
||||
}
|
||||
|
||||
function resolveOptionalManifestPath(
|
||||
inputPath: string | undefined,
|
||||
manifestPath: string
|
||||
): string | undefined {
|
||||
if (!inputPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolve(join(manifestPath, ".."), inputPath);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ The current implementation is intentionally small:
|
||||
|
||||
This is the benchmark entrypoint for prompt and artifact evaluation.
|
||||
|
||||
The CLI benchmark workspace now uses the same shared AI-guidance writer as
|
||||
`wmill init`, so benchmark runs and local CLI bootstraps go through the same
|
||||
project-guidance generation path.
|
||||
|
||||
## Usage
|
||||
|
||||
Install dependencies once:
|
||||
@@ -140,6 +144,29 @@ the current CLI does not emit them yet. Until that lands, use `compare`
|
||||
primarily to answer "did this skill bundle produce better artifacts on the same
|
||||
cases?"
|
||||
|
||||
Variant manifests can also override the top-level project instructions in
|
||||
addition to the skills bundle:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "candidate",
|
||||
"description": "Candidate guidance bundle",
|
||||
"skillsSource": {
|
||||
"type": "path",
|
||||
"path": "./snapshots/candidate-skills"
|
||||
},
|
||||
"agentsSourcePath": "./snapshots/candidate-AGENTS.md",
|
||||
"claudeSourcePath": "./snapshots/candidate-CLAUDE.md"
|
||||
}
|
||||
```
|
||||
|
||||
That matches the new `wmill init` overrides:
|
||||
|
||||
```bash
|
||||
wmill init --use-default --ai-skills-source ./ai_evals/variants/cli/snapshots/candidate-skills
|
||||
wmill init --use-default --ai-skills-source ./ai_evals/variants/cli/snapshots/candidate-skills --ai-agents-source ./my-candidate-AGENTS.md
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Later iterations should add:
|
||||
|
||||
@@ -110,6 +110,27 @@ source <(wmill completions zsh)
|
||||
|
||||
## Development
|
||||
|
||||
### AI Guidance Variants
|
||||
|
||||
`wmill init` can now materialize alternate AI guidance bundles without changing
|
||||
the generated defaults in the repo.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
wmill init --use-default --ai-skills-source /path/to/custom/skills
|
||||
wmill init --use-default --ai-skills-source /path/to/custom/skills --ai-agents-source /path/to/AGENTS.md
|
||||
wmill init --use-default --ai-skills-source /path/to/custom/skills --ai-claude-source /path/to/CLAUDE.md
|
||||
```
|
||||
|
||||
This is the same guidance-writing path used by the benchmark CLI under
|
||||
`ai_evals/`, so the benchmark harness and `wmill init` now generate the same
|
||||
project guidance shape:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `CLAUDE.md`
|
||||
- `.claude/skills/*`
|
||||
|
||||
### Testing with a local `windmill-yaml-validator`
|
||||
|
||||
To test local changes to the validator before publishing, use `npm link`:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stat, writeFile, rm, mkdir } from "node:fs/promises";
|
||||
import { stat, writeFile, rm } from "node:fs/promises";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
@@ -8,23 +8,9 @@ import { GlobalOptions } from "../../types.ts";
|
||||
import { readLockfile } from "../../utils/metadata.ts";
|
||||
import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts";
|
||||
import { generateRTNamespace } from "../resource-type/resource-type.ts";
|
||||
import { SKILLS, SKILL_CONTENT, SCHEMAS, SCHEMA_MAPPINGS } from "../../guidance/skills.ts";
|
||||
import { generateAgentsMdContent } from "../../guidance/core.ts";
|
||||
import { writeAiGuidanceFiles } from "../../guidance/writer.ts";
|
||||
import { generateCommentedTemplate } from "./template.ts";
|
||||
|
||||
/**
|
||||
* Format a YAML schema for inclusion in skill markdown files.
|
||||
*/
|
||||
function formatSchemaForMarkdown(schemaYaml: string, schemaName: string, filePattern: string): string {
|
||||
return `## ${schemaName} (\`${filePattern}\`)
|
||||
|
||||
Must be a YAML file that adheres to the following schema:
|
||||
|
||||
\`\`\`yaml
|
||||
${schemaYaml.trim()}
|
||||
\`\`\``;
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
useDefault?: boolean;
|
||||
useBackend?: boolean;
|
||||
@@ -36,6 +22,9 @@ export interface InitOptions {
|
||||
baseUrl?: string;
|
||||
configDir?: string;
|
||||
bindProfile?: boolean;
|
||||
aiSkillsSource?: string;
|
||||
aiAgentsSource?: string;
|
||||
aiClaudeSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,88 +224,24 @@ async function initAction(opts: InitOptions) {
|
||||
|
||||
// Create guidance files (AGENTS.md, CLAUDE.md, and Claude skills)
|
||||
try {
|
||||
// Generate skills reference section for AGENTS.md
|
||||
const skills_base_dir = ".claude/skills";
|
||||
const skillsReference = SKILLS.map(
|
||||
(s) => `- \`${skills_base_dir}/${s.name}/SKILL.md\` - ${s.description}`
|
||||
).join("\n");
|
||||
const guidanceResult = await writeAiGuidanceFiles({
|
||||
targetDir: ".",
|
||||
nonDottedPaths,
|
||||
overwriteProjectGuidance: false,
|
||||
skillsSourcePath: opts.aiSkillsSource,
|
||||
agentsSourcePath: opts.aiAgentsSource,
|
||||
claudeSourcePath: opts.aiClaudeSource,
|
||||
});
|
||||
|
||||
// Create AGENTS.md file with minimal instructions
|
||||
if (!(await stat("AGENTS.md").catch(() => null))) {
|
||||
await writeFile(
|
||||
"AGENTS.md",
|
||||
generateAgentsMdContent(skillsReference), "utf-8"
|
||||
);
|
||||
if (guidanceResult.agentsWritten) {
|
||||
log.info(colors.green("Created AGENTS.md"));
|
||||
}
|
||||
|
||||
// Create CLAUDE.md file, referencing AGENTS.md
|
||||
if (!(await stat("CLAUDE.md").catch(() => null))) {
|
||||
await writeFile(
|
||||
"CLAUDE.md",
|
||||
`Instructions are in @AGENTS.md
|
||||
`, "utf-8"
|
||||
);
|
||||
if (guidanceResult.claudeWritten) {
|
||||
log.info(colors.green("Created CLAUDE.md"));
|
||||
}
|
||||
|
||||
// Create .claude/skills/ directory and skill files
|
||||
try {
|
||||
await mkdir(".claude/skills", { recursive: true });
|
||||
|
||||
await Promise.all(
|
||||
SKILLS.map(async (skill) => {
|
||||
const skillDir = `.claude/skills/${skill.name}`;
|
||||
await mkdir(skillDir, { recursive: true });
|
||||
|
||||
let skillContent = SKILL_CONTENT[skill.name];
|
||||
if (skillContent) {
|
||||
// Replace placeholders with actual suffixes based on nonDottedPaths
|
||||
if (nonDottedPaths) {
|
||||
skillContent = skillContent
|
||||
.replaceAll("{{FLOW_SUFFIX}}", "__flow")
|
||||
.replaceAll("{{APP_SUFFIX}}", "__app")
|
||||
.replaceAll("{{RAW_APP_SUFFIX}}", "__raw_app")
|
||||
.replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).");
|
||||
} else {
|
||||
skillContent = skillContent
|
||||
.replaceAll("{{FLOW_SUFFIX}}", ".flow")
|
||||
.replaceAll("{{APP_SUFFIX}}", ".app")
|
||||
.replaceAll("{{RAW_APP_SUFFIX}}", ".raw_app")
|
||||
.replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files use the `.inline_script.` naming convention (e.g. `a.inline_script.ts`).");
|
||||
}
|
||||
// Check if this skill has schemas that need to be appended
|
||||
const schemaMappings = SCHEMA_MAPPINGS[skill.name];
|
||||
if (schemaMappings && schemaMappings.length > 0) {
|
||||
// Combine base content with schemas
|
||||
const schemaDocs = schemaMappings
|
||||
.map((mapping) => {
|
||||
const schemaYaml = SCHEMAS[mapping.schemaKey];
|
||||
if (schemaYaml) {
|
||||
return formatSchemaForMarkdown(schemaYaml, mapping.name, mapping.filePattern);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((doc): doc is string => doc !== null);
|
||||
|
||||
if (schemaDocs.length > 0) {
|
||||
skillContent = skillContent + "\n\n" + schemaDocs.join("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(`${skillDir}/SKILL.md`, skillContent, "utf-8");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
log.info(colors.green(`Created .claude/skills/ with ${SKILLS.length} skills`));
|
||||
} catch (skillError) {
|
||||
if (skillError instanceof Error) {
|
||||
log.warn(`Could not create skills: ${skillError.message}`);
|
||||
} else {
|
||||
log.warn(`Could not create skills: ${skillError}`);
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
colors.green(`Created .claude/skills/ with ${guidanceResult.skillCount} skills`)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
log.warn(`Could not create guidance files: ${error.message}`);
|
||||
@@ -341,6 +266,18 @@ const command = new Command()
|
||||
.description("Bootstrap a windmill project with a wmill.yaml file")
|
||||
.option("--use-default", "Use default settings without checking backend")
|
||||
.option("--use-backend", "Use backend git-sync settings if available")
|
||||
.option(
|
||||
"--ai-skills-source <path:string>",
|
||||
"Use a custom skills directory instead of the generated .claude/skills bundle"
|
||||
)
|
||||
.option(
|
||||
"--ai-agents-source <path:string>",
|
||||
"Use a custom AGENTS.md file for AI guidance"
|
||||
)
|
||||
.option(
|
||||
"--ai-claude-source <path:string>",
|
||||
"Use a custom CLAUDE.md file for AI guidance"
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when using backend settings"
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { generateAgentsMdContent } from "./core.ts";
|
||||
import {
|
||||
SCHEMAS,
|
||||
SCHEMA_MAPPINGS,
|
||||
SKILLS,
|
||||
SKILL_CONTENT,
|
||||
type SkillMetadata,
|
||||
} from "./skills.ts";
|
||||
|
||||
export interface WriteAiGuidanceOptions {
|
||||
targetDir: string;
|
||||
nonDottedPaths?: boolean;
|
||||
overwriteProjectGuidance?: boolean;
|
||||
skillsSourcePath?: string;
|
||||
agentsSourcePath?: string;
|
||||
claudeSourcePath?: string;
|
||||
}
|
||||
|
||||
export interface WriteAiGuidanceResult {
|
||||
agentsWritten: boolean;
|
||||
claudeWritten: boolean;
|
||||
skillsWritten: boolean;
|
||||
skillCount: number;
|
||||
}
|
||||
|
||||
const CLAUDE_MD_DEFAULT = "Instructions are in @AGENTS.md\n";
|
||||
|
||||
export async function writeAiGuidanceFiles(
|
||||
options: WriteAiGuidanceOptions
|
||||
): Promise<WriteAiGuidanceResult> {
|
||||
const nonDottedPaths = options.nonDottedPaths ?? true;
|
||||
const skillMetadata = options.skillsSourcePath
|
||||
? await copySkillsFromSource(options.targetDir, options.skillsSourcePath)
|
||||
: await writeGeneratedSkills(options.targetDir, nonDottedPaths);
|
||||
|
||||
const agentsWritten = await writeProjectGuidanceFile({
|
||||
targetPath: join(options.targetDir, "AGENTS.md"),
|
||||
overwrite: options.overwriteProjectGuidance ?? false,
|
||||
content:
|
||||
options.agentsSourcePath != null
|
||||
? await readFile(options.agentsSourcePath, "utf8")
|
||||
: generateAgentsMdContent(buildSkillsReference(skillMetadata)),
|
||||
});
|
||||
|
||||
const claudeWritten = await writeProjectGuidanceFile({
|
||||
targetPath: join(options.targetDir, "CLAUDE.md"),
|
||||
overwrite: options.overwriteProjectGuidance ?? false,
|
||||
content:
|
||||
options.claudeSourcePath != null
|
||||
? await readFile(options.claudeSourcePath, "utf8")
|
||||
: CLAUDE_MD_DEFAULT,
|
||||
});
|
||||
|
||||
return {
|
||||
agentsWritten,
|
||||
claudeWritten,
|
||||
skillsWritten: true,
|
||||
skillCount: skillMetadata.length,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSkillsReference(skills: Pick<SkillMetadata, "name" | "description">[]): string {
|
||||
return skills
|
||||
.map((skill) => `- \`.claude/skills/${skill.name}/SKILL.md\` - ${skill.description}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function copySkillsFromSource(
|
||||
targetDir: string,
|
||||
skillsSourcePath: string
|
||||
): Promise<SkillMetadata[]> {
|
||||
const skillsDir = join(targetDir, ".claude", "skills");
|
||||
await mkdir(join(targetDir, ".claude"), { recursive: true });
|
||||
await rm(skillsDir, { recursive: true, force: true });
|
||||
await cp(skillsSourcePath, skillsDir, { recursive: true, force: true });
|
||||
return await readSkillMetadataFromDirectory(skillsDir);
|
||||
}
|
||||
|
||||
async function writeGeneratedSkills(
|
||||
targetDir: string,
|
||||
nonDottedPaths: boolean
|
||||
): Promise<SkillMetadata[]> {
|
||||
const skillsDir = join(targetDir, ".claude", "skills");
|
||||
await rm(skillsDir, { recursive: true, force: true });
|
||||
await mkdir(skillsDir, { recursive: true });
|
||||
|
||||
await Promise.all(
|
||||
SKILLS.map(async (skill) => {
|
||||
const skillDir = join(skillsDir, skill.name);
|
||||
await mkdir(skillDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(skillDir, "SKILL.md"),
|
||||
renderGeneratedSkillContent(skill.name, nonDottedPaths),
|
||||
"utf8"
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return SKILLS;
|
||||
}
|
||||
|
||||
function renderGeneratedSkillContent(skillName: string, nonDottedPaths: boolean): string {
|
||||
let skillContent = SKILL_CONTENT[skillName];
|
||||
if (!skillContent) {
|
||||
throw new Error(`Missing generated skill content for ${skillName}`);
|
||||
}
|
||||
|
||||
if (nonDottedPaths) {
|
||||
skillContent = skillContent
|
||||
.replaceAll("{{FLOW_SUFFIX}}", "__flow")
|
||||
.replaceAll("{{APP_SUFFIX}}", "__app")
|
||||
.replaceAll("{{RAW_APP_SUFFIX}}", "__raw_app")
|
||||
.replaceAll(
|
||||
"{{INLINE_SCRIPT_NAMING}}",
|
||||
"Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`)."
|
||||
);
|
||||
} else {
|
||||
skillContent = skillContent
|
||||
.replaceAll("{{FLOW_SUFFIX}}", ".flow")
|
||||
.replaceAll("{{APP_SUFFIX}}", ".app")
|
||||
.replaceAll("{{RAW_APP_SUFFIX}}", ".raw_app")
|
||||
.replaceAll(
|
||||
"{{INLINE_SCRIPT_NAMING}}",
|
||||
"Inline script files use the `.inline_script.` naming convention (e.g. `a.inline_script.ts`)."
|
||||
);
|
||||
}
|
||||
|
||||
const schemaMappings = SCHEMA_MAPPINGS[skillName];
|
||||
if (!schemaMappings || schemaMappings.length === 0) {
|
||||
return skillContent;
|
||||
}
|
||||
|
||||
const schemaDocs = schemaMappings
|
||||
.map((mapping) => {
|
||||
const schemaYaml = SCHEMAS[mapping.schemaKey];
|
||||
if (!schemaYaml) {
|
||||
return null;
|
||||
}
|
||||
return formatSchemaForMarkdown(schemaYaml, mapping.name, mapping.filePattern);
|
||||
})
|
||||
.filter((entry): entry is string => entry !== null);
|
||||
|
||||
if (schemaDocs.length === 0) {
|
||||
return skillContent;
|
||||
}
|
||||
|
||||
return `${skillContent}\n\n${schemaDocs.join("\n\n")}`;
|
||||
}
|
||||
|
||||
async function readSkillMetadataFromDirectory(skillsDir: string): Promise<SkillMetadata[]> {
|
||||
const entries = await readdir(skillsDir, { withFileTypes: true });
|
||||
const skills: SkillMetadata[] = [];
|
||||
|
||||
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const skillPath = join(skillsDir, entry.name, "SKILL.md");
|
||||
if (!(await stat(skillPath).catch(() => null))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = await readFile(skillPath, "utf8");
|
||||
skills.push(parseSkillMetadata(content, entry.name));
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
function parseSkillMetadata(content: string, fallbackName: string): SkillMetadata {
|
||||
const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
||||
if (!frontMatterMatch) {
|
||||
return {
|
||||
name: fallbackName,
|
||||
description: `Skill loaded from ${fallbackName}`,
|
||||
};
|
||||
}
|
||||
|
||||
let name = fallbackName;
|
||||
let description = `Skill loaded from ${fallbackName}`;
|
||||
|
||||
for (const line of frontMatterMatch[1].split("\n")) {
|
||||
const separatorIndex = line.indexOf(":");
|
||||
if (separatorIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
const value = line.slice(separatorIndex + 1).trim();
|
||||
|
||||
if (key === "name" && value) {
|
||||
name = value;
|
||||
} else if (key === "description" && value) {
|
||||
description = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { name, description };
|
||||
}
|
||||
|
||||
async function writeProjectGuidanceFile(options: {
|
||||
targetPath: string;
|
||||
content: string;
|
||||
overwrite: boolean;
|
||||
}): Promise<boolean> {
|
||||
if (!options.overwrite && (await stat(options.targetPath).catch(() => null))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await writeFile(options.targetPath, options.content, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
function formatSchemaForMarkdown(
|
||||
schemaYaml: string,
|
||||
schemaName: string,
|
||||
filePattern: string
|
||||
): string {
|
||||
return `## ${schemaName} (\`${filePattern}\`)
|
||||
|
||||
Must be a YAML file that adheres to the following schema:
|
||||
|
||||
\`\`\`yaml
|
||||
${schemaYaml.trim()}
|
||||
\`\`\``;
|
||||
}
|
||||
Reference in New Issue
Block a user