diff --git a/docs/installation.md b/docs/installation.md index 7fcfc5f7..c8b22cfc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -234,8 +234,9 @@ entry points. on this SDK by version range), a strict `tsconfig.json`, `src/agent.ts` calling `createAgent({ model, instructions })` with an example `defineTool()` tool, an offline `src/agent.test.ts` using `mockModel`, a `.env.example` naming your -provider's key variable, `.gitignore` and a README. It then installs the -dependencies and runs `git init`. +provider's key variable, `.gitignore`, a README, and an `AGENTS.md` (plus a +one-line `CLAUDE.md` importing it) telling coding agents the project's files, +scripts and conventions. It then installs the dependencies and runs `git init`. ```bash npm create lousho-agent my-agent # or: npx @lousho/build-ai-agent init my-agent diff --git a/llms-full.txt b/llms-full.txt index 8cec3f9a..d52b2a8c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -595,8 +595,9 @@ entry points. on this SDK by version range), a strict `tsconfig.json`, `src/agent.ts` calling `createAgent({ model, instructions })` with an example `defineTool()` tool, an offline `src/agent.test.ts` using `mockModel`, a `.env.example` naming your -provider's key variable, `.gitignore` and a README. It then installs the -dependencies and runs `git init`. +provider's key variable, `.gitignore`, a README, and an `AGENTS.md` (plus a +one-line `CLAUDE.md` importing it) telling coding agents the project's files, +scripts and conventions. It then installs the dependencies and runs `git init`. ```bash npm create lousho-agent my-agent # or: npx @lousho/build-ai-agent init my-agent diff --git a/src/cli/init/templates.test.ts b/src/cli/init/templates.test.ts index 9653ef4b..485ff0d0 100644 --- a/src/cli/init/templates.test.ts +++ b/src/cli/init/templates.test.ts @@ -44,7 +44,7 @@ describe('renderProject', () => { it.each(combos)('%s / %s: file set and key contents', (provider, template) => { const files = renderProject(config(provider, template)); - const expected = ['.env.example', '.gitignore', 'README.md', 'package.json', 'src/agent.test.ts', 'tsconfig.json']; + const expected = ['.env.example', '.gitignore', 'AGENTS.md', 'CLAUDE.md', 'README.md', 'package.json', 'src/agent.test.ts', 'tsconfig.json']; expected.push(...(template === 'yaml' ? ['agent.yaml'] : ['src/agent.ts', 'src/index.ts'])); expect(Object.keys(files).sort()).toEqual(expected.sort()); @@ -98,6 +98,30 @@ describe('renderProject', () => { } ); + it.each(combos)('%s / %s: AGENTS.md is short, lists the real scripts and only lousho.com links', (provider, template) => { + const files = renderProject(config(provider, template)); + const agents = files['AGENTS.md']!; + expect(agents.trimEnd().split('\n').length).toBeLessThanOrEqual(60); + const scripts = JSON.parse(files['package.json']!).scripts as Record; + const listed = [...agents.matchAll(/^- `npm run ([\w:-]+)`/gm)].map((m) => m[1]); + expect(listed.sort()).toEqual(Object.keys(scripts).sort()); + expect(agents).toContain(ENV_KEYS[provider]); + expect(agents).toContain(template === 'yaml' ? 'agent.yaml' : 'defineTool'); + if (template !== 'yaml') { + for (const word of ['needsApproval', 'mockModel', 'maxTokens', `${provider}/`]) expect(agents).toContain(word); + } + const urls = [...agents.matchAll(/https?:\/\/[^\s)]+/g)].map((m) => m[0]); + expect(urls).toContain('https://lousho.com/llms.txt'); + for (const url of urls) expect(url).toMatch(/^https:\/\/lousho\.com\//); + expect(files['CLAUDE.md']).toBe('@AGENTS.md\n'); + }); + + it('AGENTS.md uses the chosen package manager', () => { + const agents = renderProject({ ...config('openai', 'minimal'), packageManager: 'pnpm' })['AGENTS.md']!; + expect(agents).toContain('`pnpm run test`'); + expect(agents).not.toMatch(/(^|[s`])npm run/); + }); + it('the tools template defines more tools than minimal', () => { const count = (template: Template) => (renderProject(config('openai', template))['src/agent.ts']!.match(/defineTool\(\{/g) ?? []).length; diff --git a/src/cli/init/templates.ts b/src/cli/init/templates.ts index cbbd0d64..0ab12531 100644 --- a/src/cli/init/templates.ts +++ b/src/cli/init/templates.ts @@ -55,25 +55,30 @@ function zodRange(peerRange: string | undefined, provider: string): string { return provider === 'ollama' ? (alternatives.find((part) => part.startsWith('^4')) ?? '^4.0.0') : range; } +/** The generated package.json's scripts; AGENTS.md lists exactly these. */ +function scripts(config: ProjectConfig): Record { + const yaml = config.template === 'yaml'; + return { + // `lousho dev` loads .env itself, and so does src/index.ts (tsx --env-file-if-exists prints "not found" twice). + dev: yaml ? 'lousho dev agent.yaml' : 'tsx src/index.ts', + // The chat UI with hot reload; it serves src/agent.ts's default export. + ...(yaml ? {} : { 'dev:ui': 'lousho dev src/agent.ts' }), + test: 'vitest run', + typecheck: 'tsc --noEmit', + doctor: yaml ? 'lousho doctor agent.yaml' : 'lousho doctor src/agent.ts', + // A self-contained HTTP server in .lousho/build/node-server (--target=docker adds a Dockerfile). + build: `lousho build ${yaml ? 'agent.yaml' : 'src/agent.ts'} --target=node-server`, + }; +} + function packageJson(config: ProjectConfig): string { const peers = config.sdk.peerDependencies ?? {}; - const yaml = config.template === 'yaml'; const pkg = { name: config.name, version: '0.1.0', private: true, type: 'module', - scripts: { - // `lousho dev` loads .env itself, and so does src/index.ts (tsx --env-file-if-exists prints "not found" twice). - dev: yaml ? 'lousho dev agent.yaml' : 'tsx src/index.ts', - // The chat UI with hot reload; it serves src/agent.ts's default export. - ...(yaml ? {} : { 'dev:ui': 'lousho dev src/agent.ts' }), - test: 'vitest run', - typecheck: 'tsc --noEmit', - doctor: yaml ? 'lousho doctor agent.yaml' : 'lousho doctor src/agent.ts', - // A self-contained HTTP server in .lousho/build/node-server (--target=docker adds a Dockerfile). - build: `lousho build ${yaml ? 'agent.yaml' : 'src/agent.ts'} --target=node-server`, - }, + scripts: scripts(config), dependencies: sortKeys({ '@lousho/build-ai-agent': config.sdkDependency, zod: zodRange(peers.zod, config.provider), @@ -335,6 +340,63 @@ const TROUBLESHOOTING = `## Troubleshooting `; +const SCRIPT_NOTES: Record = { + dev: 'chat with the agent', + 'dev:ui': 'chat UI with hot reload', + test: 'offline tests (vitest), no API key needed', + typecheck: 'tsc --noEmit', + doctor: 'check Node, packages and the API key', + build: 'bundle a deployable HTTP server into .lousho/build/node-server', +}; + +/** AGENTS.md: short, project-specific guidance for coding agents (Claude Code, Cursor, Codex). */ +function agentsMd(config: ProjectConfig): string { + const pm = config.packageManager; + const info = providerInfo(config.provider); + const yaml = config.template === 'yaml'; + const commands = Object.keys(scripts(config)) + .map((name) => `- \`${run(pm, name)}\` - ${SCRIPT_NOTES[name] ?? name}`) + .join('\n'); + const files = yaml + ? '- `agent.yaml` - the agent: prompt, provider, model and tools (built-in tool names, e.g. `current-date`)\n- `src/agent.test.ts` - checks the spec loads, offline' + : '- `src/agent.ts` - the agent (default export is its config) and its tools\n- `src/agent.test.ts` - tests with `mockModel`, offline\n- `src/index.ts` - the terminal chat (`dev`)'; + const conventions = yaml + ? `- Edit \`agent.yaml\` to change the agent; \`model\` is a model id of the \`${config.provider}\` provider. +- Keep tests offline: \`loadSpec\` the YAML and assert on it; never call a live model in tests.` + : `- Define tools with \`defineTool({ name, description, input: z.object(...), execute })\`; \`input\` is a zod schema. +- Give any tool with side effects \`needsApproval: true\` (or a predicate) so a human approves the call. +- The model string is \`provider/model\`, here \`${config.provider}/${info.defaultModel}\`. +- Replies are capped by \`modelSettings.maxTokens\` (4096) in \`src/agent.ts\`; raise it for longer answers. +- Test offline with \`mockModel([...])\` from \`@lousho/build-ai-agent/testing\`; never call a live model in tests.`; + return `# ${config.name} + +An agent built with \`@lousho/build-ai-agent\` (provider: ${config.provider}). Read this before changing code. + +## Files + +${files} +- \`.env\` - secrets (${info.envKey}); copy from \`.env.example\`, never commit it + +## Commands + +${commands} + +Run \`${run(pm, 'test')}\` and \`${run(pm, 'typecheck')}\` after every change. + +## Conventions + +${conventions} + +## Docs + +- https://lousho.com/llms.txt - index of the docs, written for coding agents +- https://lousho.com/quickstart +- https://lousho.com/tools +- https://lousho.com/approvals +- https://lousho.com/testing +`; +} + function readme(config: ProjectConfig): string { const pm = config.packageManager; const info = providerInfo(config.provider); @@ -378,6 +440,9 @@ export function renderProject(config: ProjectConfig): Record { '.gitignore': gitignore(config), '.env.example': envExample(config), 'README.md': readme(config), + 'AGENTS.md': agentsMd(config), + // Claude Code reads CLAUDE.md, not AGENTS.md; `@AGENTS.md` imports it. + 'CLAUDE.md': '@AGENTS.md\n', 'src/agent.test.ts': yaml ? yamlTestSource(config) : testSource(), }; if (yaml) {