diff --git a/AGENTS.md b/AGENTS.md index a9e3dd8..cddc12a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -211,6 +211,17 @@ writes nothing — so check the files landed rather than trusting the exit code. `.agents/` is deliberately **not** in the generated `.gitignore`: the skill is meant to be committed so the whole team gets the same copy. +### When the skill is absent + +The generated README and `AGENTS.md` both have a skill section, and it must +describe what is on disk. `project-generator.ts` therefore copies the skill +**before** rendering any template and passes `withSkills: skillCopied` — the +copy's actual outcome, not the flag, so a failed copy is covered too. Without +the skill, both docs link upstream (`SKILL_URL`) and give `SKILLS_ADD_COMMAND` +instead of pointing at `.agents/`. That command omits `-a universal` on purpose: +whoever opted out gets to pick the agent. The README section lives in +`src/templates/common/skill-readme.ts`, shared by both transports. + ## Examples `examples/` holds complete servers built with the CLI, one folder per example, indexed in `examples/README.md`. They are not part of the published package (`files` is `dist` only) and the root `lint`, `format`, `tsc` and `test` scripts do not touch them. diff --git a/src/project-generator.ts b/src/project-generator.ts index 7394668..cb37dd6 100644 --- a/src/project-generator.ts +++ b/src/project-generator.ts @@ -49,18 +49,34 @@ export interface ProjectConfig { export async function generateProject(config: ProjectConfig): Promise { const { projectName, packageManager, transport, withOAuth, withGitInit, withSkills } = config; - const templateOptions: CommonTemplateOptions = { - withOAuth: transport === 'http' ? withOAuth : false, - packageManager, - transport, - }; - const projectPath = join(process.cwd(), projectName); const srcPath = join(projectPath, 'src'); // Create directories await mkdir(srcPath, { recursive: true }); + // The skill is vendored, so this is a file copy. It never fails the scaffold: + // the project is complete without it. It runs before the templates so the + // README and AGENTS.md describe what is actually on disk — a project without + // the skill must not point at a directory that does not exist. + let skillCopied = false; + if (withSkills) { + const result = await copySkill(projectPath); + skillCopied = result.copied; + + if (!result.copied) { + console.log(`\n Could not add the tool-design skill (${result.reason}).`); + console.log(` To add it by hand: ${SKILLS_INSTALL_HINT}`); + } + } + + const templateOptions: CommonTemplateOptions = { + withOAuth: transport === 'http' ? withOAuth : false, + packageManager, + transport, + withSkills: skillCopied, + }; + // Build list of files to write const filesToWrite: Promise[] = []; @@ -132,19 +148,6 @@ export async function generateProject(config: ProjectConfig): Promise { } } - // The skill is vendored, so this is a file copy. It never fails the scaffold: - // the project is complete without it. - let skillCopied = false; - if (withSkills) { - const result = await copySkill(projectPath); - skillCopied = result.copied; - - if (!result.copied) { - console.log(`\n Could not add the tool-design skill (${result.reason}).`); - console.log(` To add it by hand: ${SKILLS_INSTALL_HINT}`); - } - } - const commands = packageManagerCommands[packageManager]; console.log(`\nCreated ${projectName} with MCP SDK (${transport}) at ${projectPath}`); console.log(`\nNext steps:`); diff --git a/src/skills.ts b/src/skills.ts index bfbe227..4d5141b 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -32,6 +32,17 @@ export const SKILLS_UPDATE_COMMAND = 'npx skills update -p -y'; */ export const SKILLS_INSTALL_HINT = `npx skills add ${SKILL_REPO} --skill ${SKILL_NAME} -a universal`; +/** Where the skill lives upstream, for projects scaffolded without it. */ +export const SKILL_URL = `https://github.com/${SKILL_REPO}/tree/main/${SKILL_NAME}`; + +/** + * Offered to projects scaffolded with `--no-skills`. + * + * No `-a universal` here: whoever opted out of the vendored copy gets to pick + * which agent the skills CLI installs for. + */ +export const SKILLS_ADD_COMMAND = `npx skills add ${SKILL_REPO} --skill ${SKILL_NAME}`; + /** * The vendored skill, copied verbatim from `agentailor/skills`. * diff --git a/src/templates/common/agents.md.test.ts b/src/templates/common/agents.md.test.ts index 1c07008..85e7dab 100644 --- a/src/templates/common/agents.md.test.ts +++ b/src/templates/common/agents.md.test.ts @@ -136,6 +136,17 @@ describe('getAgentsMdTemplate', () => { expect(template).toContain('Commit `.agents/`'); }); + // With --no-skills there is no .agents/ to point at; send the reader upstream. + it('should link upstream and give the install command when the skill is absent', () => { + for (const options of [sdkHttp, sdkStdio]) { + const template = getAgentsMdTemplate(projectName, { ...options, withSkills: false }); + expect(template).not.toContain('.agents/'); + expect(template).not.toContain('npx skills update'); + expect(template).toContain('https://github.com/agentailor/skills/tree/main/tool-design'); + expect(template).toContain('npx skills add agentailor/skills --skill tool-design'); + } + }); + // The copy is vendored, so updating is the user's choice, not automatic. it("should frame the update as the reader's choice", () => { const template = getAgentsMdTemplate(projectName, sdkHttp); diff --git a/src/templates/common/agents.md.ts b/src/templates/common/agents.md.ts index 375fa4a..e3c86e7 100644 --- a/src/templates/common/agents.md.ts +++ b/src/templates/common/agents.md.ts @@ -1,5 +1,11 @@ import type { CommonTemplateOptions } from './types.js'; -import { SKILLS_UPDATE_COMMAND, SKILLS_INSTALL_HINT, SKILLS_DIR } from '../../skills.js'; +import { + SKILLS_UPDATE_COMMAND, + SKILLS_INSTALL_HINT, + SKILLS_DIR, + SKILLS_ADD_COMMAND, + SKILL_URL, +} from '../../skills.js'; /** * AGENTS.md for the generated project. @@ -12,6 +18,7 @@ export function getAgentsMdTemplate(projectName: string, options?: CommonTemplat const transport = options?.transport ?? 'http'; const withOAuth = options?.withOAuth ?? false; const packageManager = options?.packageManager ?? 'npm'; + const withSkills = options?.withSkills ?? true; const isStdio = transport === 'stdio'; @@ -152,7 +159,8 @@ root, and domain logic stays free of MCP imports so it remains testable on its own. `; - const skillSection = ` + const skillSection = withSkills + ? ` ## The tool-design skill \`${SKILLS_DIR}/skills/tool-design/SKILL.md\` is the long form of the guidance above, @@ -172,6 +180,17 @@ That is yours to run, not something this project does for you. If the directory is missing entirely, add it with \`${SKILLS_INSTALL_HINT}\`. Commit \`${SKILLS_DIR}/\` so everyone working on this project gets the same copy. +` + : ` +## The tool-design skill + +The guidance above is the short form of Agentailor's +[tool-design](${SKILL_URL}) skill, which has the long form with worked +examples. It is not installed in this project. To add it: + +\`\`\`bash +${SKILLS_ADD_COMMAND} +\`\`\` `; const conventions = ` diff --git a/src/templates/common/skill-readme.test.ts b/src/templates/common/skill-readme.test.ts new file mode 100644 index 0000000..0495d48 --- /dev/null +++ b/src/templates/common/skill-readme.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { getSkillReadmeSection } from './skill-readme.js'; +import { getReadmeTemplate as getHttpReadme } from '../sdk/http/index.js'; +import { getReadmeTemplate as getStdioReadme } from '../sdk/stdio/index.js'; + +describe('getSkillReadmeSection', () => { + it('should point at the installed skill and how to update it', () => { + const section = getSkillReadmeSection(true); + expect(section).toContain('.agents/skills/tool-design/'); + expect(section).toContain('npx skills update -p -y'); + }); + + // A --no-skills project has no .agents/; send the reader upstream instead. + it('should link upstream and give the install command when the skill is absent', () => { + const section = getSkillReadmeSection(false); + expect(section).not.toContain('.agents/'); + expect(section).not.toContain('npx skills update'); + expect(section).toContain('https://github.com/agentailor/skills/tree/main/tool-design'); + expect(section).toContain('npx skills add agentailor/skills --skill tool-design'); + }); + + it('should follow withSkills in both transport READMEs', () => { + for (const getReadme of [getHttpReadme, getStdioReadme]) { + expect(getReadme('p')).toContain('.agents/skills/tool-design/'); + expect(getReadme('p', { withSkills: false })).not.toContain('.agents/'); + } + }); +}); diff --git a/src/templates/common/skill-readme.ts b/src/templates/common/skill-readme.ts new file mode 100644 index 0000000..ff315d8 --- /dev/null +++ b/src/templates/common/skill-readme.ts @@ -0,0 +1,37 @@ +import { SKILLS_ADD_COMMAND, SKILLS_DIR, SKILLS_UPDATE_COMMAND, SKILL_URL } from '../../skills.js'; + +/** + * The README's "Tool design skill" section, shared by every transport. + * + * Branches on whether the skill is actually in the project: pointing at + * `.agents/skills/tool-design/` in a project scaffolded with `--no-skills` sends + * the reader to a directory that does not exist. + */ +export function getSkillReadmeSection(withSkills: boolean): string { + if (!withSkills) { + return `## Tool design skill + +Agentailor's [tool-design](${SKILL_URL}) skill gives a coding agent working here +detailed guidance on designing MCP tools. It is not installed in this project. +To add it: + +\`\`\`bash +${SKILLS_ADD_COMMAND} +\`\`\` +`; + } + + return `## Tool design skill + +Agentailor's [tool-design](${SKILL_URL}) skill is +included at \`${SKILLS_DIR}/skills/tool-design/\`, so a coding agent working here has +the guidance on hand. Commit it so the whole team gets the same copy. + +It is the version that shipped with the scaffolding CLI. To check for a newer +one — your call, not automatic: + +\`\`\`bash +${SKILLS_UPDATE_COMMAND} +\`\`\` +`; +} diff --git a/src/templates/common/types.ts b/src/templates/common/types.ts index a0b70ea..eb9e533 100644 --- a/src/templates/common/types.ts +++ b/src/templates/common/types.ts @@ -6,6 +6,11 @@ export type TransportType = 'http' | 'stdio'; */ export interface BaseTemplateOptions { packageManager?: PackageManager; + /** + * Whether the tool-design skill is present in the project. Defaults to true. + * The generator sets it from the copy's actual outcome, not just the flag. + */ + withSkills?: boolean; } /** diff --git a/src/templates/sdk/http/readme.ts b/src/templates/sdk/http/readme.ts index 5084889..455e1a8 100644 --- a/src/templates/sdk/http/readme.ts +++ b/src/templates/sdk/http/readme.ts @@ -1,4 +1,5 @@ import type { TemplateOptions } from './index.js'; +import { getSkillReadmeSection } from '../../common/skill-readme.js'; /** * Shared README template, used by both SDK HTTP template types. @@ -245,19 +246,7 @@ docker run -p 3000:3000 ${projectName} instance is discarded between calls. - Modify the HTTP handler and Express configuration in \`src/index.ts\`${customizationOAuthNote} -## Tool design skill - -Agentailor's [tool-design](https://github.com/agentailor/skills) skill is -included at \`.agents/skills/tool-design/\`, so a coding agent working here has -the guidance on hand. Commit it so the whole team gets the same copy. - -It is the version that shipped with the scaffolding CLI. To check for a newer -one — your call, not automatic: - -\`\`\`bash -npx skills update -p -y -\`\`\` - +${getSkillReadmeSection(options?.withSkills ?? true)} ## Learn More - [Model Context Protocol](https://modelcontextprotocol.io/) diff --git a/src/templates/sdk/stdio/readme.ts b/src/templates/sdk/stdio/readme.ts index e7b4b69..c579a48 100644 --- a/src/templates/sdk/stdio/readme.ts +++ b/src/templates/sdk/stdio/readme.ts @@ -1,4 +1,5 @@ import type { TemplateOptions } from './index.js'; +import { getSkillReadmeSection } from '../../common/skill-readme.js'; export function getReadmeTemplate(projectName: string, options?: TemplateOptions): string { const packageManager = options?.packageManager ?? 'npm'; @@ -155,19 +156,7 @@ ${projectName}/ imports so it stays testable on its own. - Modify transport configuration in \`src/index.ts\` -## Tool design skill - -Agentailor's [tool-design](https://github.com/agentailor/skills) skill is -included at \`.agents/skills/tool-design/\`, so a coding agent working here has -the guidance on hand. Commit it so the whole team gets the same copy. - -It is the version that shipped with the scaffolding CLI. To check for a newer -one — your call, not automatic: - -\`\`\`bash -npx skills update -p -y -\`\`\` - +${getSkillReadmeSection(options?.withSkills ?? true)} ## Learn More - [Model Context Protocol](https://modelcontextprotocol.io/)