Skip to content
Merged
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
5 changes: 3 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion src/cli/init/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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<string, string>;
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;
Expand Down
89 changes: 77 additions & 12 deletions src/cli/init/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
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),
Expand Down Expand Up @@ -335,6 +340,63 @@ const TROUBLESHOOTING = `## Troubleshooting

`;

const SCRIPT_NOTES: Record<string, string> = {
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);
Expand Down Expand Up @@ -378,6 +440,9 @@ export function renderProject(config: ProjectConfig): Record<string, string> {
'.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) {
Expand Down
Loading