diff --git a/examples/bug-fixer/cli.ts b/examples/bug-fixer/cli.ts new file mode 100755 index 0000000..4f5f4f4 --- /dev/null +++ b/examples/bug-fixer/cli.ts @@ -0,0 +1,107 @@ +#!/usr/bin/env bun + +/** + * Bug Fixer CLI + * + * A persistent agent that finds and fixes bugs in code. + * + * Usage: + * bun cli.ts "description of the bug" # Fix a specific bug + * bun cli.ts # Interactive mode + * bun cli.ts --status # Show agent status + * bun cli.ts --reset # Reset agent + */ + +import { parseArgs } from 'node:util'; +import { + loadState, + saveState, + getOrCreateAgent, + fixBug, + interactiveMode, + showStatus, + reset, +} from './fixer.js'; + +async function main() { + const { values, positionals } = parseArgs({ + args: process.argv.slice(2), + options: { + status: { type: 'boolean', default: false }, + reset: { type: 'boolean', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + allowPositionals: true, + }); + + if (values.help) { + printHelp(); + return; + } + + if (values.reset) { + await reset(); + return; + } + + const state = await loadState(); + + if (values.status) { + await showStatus(state); + return; + } + + // Get or create the agent + const agent = await getOrCreateAgent(state); + + // Save agent ID if new + if (!state.agentId && agent.agentId) { + state.agentId = agent.agentId; + await saveState(state); + console.log(`\x1b[90m[Agent: ${agent.agentId}]\x1b[0m`); + console.log(`\x1b[90m[→ https://app.letta.com/agents/${agent.agentId}]\x1b[0m\n`); + } + + if (positionals.length > 0) { + // Fix the specified bug + const bugDescription = positionals.join(' '); + await fixBug(agent, state, bugDescription); + } else { + // Interactive mode + await interactiveMode(agent, state); + } + + agent.close(); +} + +function printHelp() { + console.log(` +šŸ› Bug Fixer + +A persistent agent that finds and fixes bugs. Remembers your codebase. + +USAGE: + bun cli.ts [bug description] Fix a specific bug + bun cli.ts Interactive mode + bun cli.ts --status Show agent status + bun cli.ts --reset Reset agent (forget everything) + bun cli.ts -h, --help Show this help + +EXAMPLES: + bun cli.ts "the tests in auth.test.ts are failing" + bun cli.ts "TypeError on line 42 of utils.ts" + bun cli.ts "npm run build shows an error about missing module" + +HOW IT WORKS: + 1. Describe the bug (error message, failing test, unexpected behavior) + 2. The agent explores your codebase to understand the context + 3. It identifies the root cause and makes a fix + 4. It runs tests or commands to verify + +PERSISTENCE: + The agent remembers your codebase across sessions. The more you use it, + the better it knows where things are and what patterns to look for. +`); +} + +main().catch(console.error); diff --git a/examples/bug-fixer/fixer.ts b/examples/bug-fixer/fixer.ts new file mode 100644 index 0000000..c5a9c3b --- /dev/null +++ b/examples/bug-fixer/fixer.ts @@ -0,0 +1,255 @@ +/** + * Bug Fixer Agent + * + * A persistent agent that finds and fixes bugs in code. + * Remembers the codebase and past fixes across sessions. + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createSession, resumeSession, type Session } from '../../src/index.js'; +import { BugFixerState, DEFAULT_CONFIG } from './types.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const STATE_FILE = join(__dirname, 'state.json'); + +// ANSI colors +const COLORS = { + agent: '\x1b[36m', // Cyan + system: '\x1b[90m', // Gray + success: '\x1b[32m', // Green + error: '\x1b[31m', // Red + reset: '\x1b[0m', +}; + +const SYSTEM_PROMPT = `You are a bug-fixing agent. Your job is to find and fix bugs in code. + +## Your Capabilities +You have access to tools for exploring and modifying code: +- Read files to understand the codebase +- Search for patterns with Grep +- Find files with Glob +- Edit files to fix bugs +- Run commands with Bash (tests, linters, etc.) + +## Your Approach +1. **Understand the bug** - Read the error message or failing test carefully +2. **Explore the codebase** - Find relevant files and understand the context +3. **Identify the root cause** - Don't just fix symptoms, find the actual issue +4. **Make minimal changes** - Fix the bug without unnecessary refactoring +5. **Verify the fix** - Run tests or the command that was failing + +## Memory +You remember past sessions. Use this to: +- Recall where things are in the codebase +- Remember patterns that caused bugs before +- Avoid repeating failed approaches + +## Style +- Be concise but thorough +- Explain what you're doing and why +- If you're unsure, say so and explain your reasoning`; + +/** + * Load state from disk + */ +export async function loadState(): Promise { + if (existsSync(STATE_FILE)) { + const data = await readFile(STATE_FILE, 'utf-8'); + return JSON.parse(data); + } + return { + agentId: null, + fixCount: 0, + }; +} + +/** + * Save state to disk + */ +export async function saveState(state: BugFixerState): Promise { + await writeFile(STATE_FILE, JSON.stringify(state, null, 2)); +} + +/** + * Get or create the bug fixer agent + */ +export async function getOrCreateAgent(state: BugFixerState): Promise { + if (state.agentId) { + console.log(`${COLORS.system}Resuming bug fixer agent...${COLORS.reset}`); + return resumeSession(state.agentId, { + model: DEFAULT_CONFIG.model, + allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], + permissionMode: 'bypassPermissions', + }); + } + + console.log(`${COLORS.system}Creating new bug fixer agent...${COLORS.reset}`); + const session = await createSession({ + model: DEFAULT_CONFIG.model, + systemPrompt: SYSTEM_PROMPT, + memory: [ + { + label: 'codebase-knowledge', + value: `# Codebase Knowledge + +## Project Structure +(Will be populated as I explore) + +## Key Files +(Important files I've discovered) + +## Patterns +(Common patterns in this codebase)`, + description: 'What I know about this codebase', + }, + { + label: 'fix-history', + value: `# Fix History + +## Past Bugs +(Bugs I've fixed before) + +## Lessons Learned +(What I've learned from past fixes)`, + description: 'History of bugs fixed and lessons learned', + }, + ], + allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], + permissionMode: 'bypassPermissions', + }); + + return session; +} + +/** + * Stream output with color + */ +function createStreamPrinter(): (text: string) => void { + return (text: string) => { + process.stdout.write(`${COLORS.agent}${text}${COLORS.reset}`); + }; +} + +/** + * Send a message and get response + */ +export async function chat( + session: Session, + message: string, + onOutput?: (text: string) => void +): Promise { + await session.send(message); + + let response = ''; + const printer = onOutput || createStreamPrinter(); + + for await (const msg of session.receive()) { + if (msg.type === 'assistant') { + response += msg.content; + printer(msg.content); + } else if (msg.type === 'tool_call') { + console.log(`\n${COLORS.system}[${msg.name}]${COLORS.reset}`); + } else if (msg.type === 'tool_result') { + console.log(`${COLORS.system}[done]${COLORS.reset}\n`); + } + } + + return response; +} + +/** + * Fix a bug + */ +export async function fixBug( + session: Session, + state: BugFixerState, + description: string +): Promise { + console.log(`\n${COLORS.system}Starting bug fix...${COLORS.reset}\n`); + + const prompt = `Please fix this bug: + +${description} + +Steps: +1. First, explore the codebase to understand the context +2. Find the root cause of the bug +3. Make the minimal fix needed +4. Run any relevant tests to verify + +Go ahead and fix it.`; + + await chat(session, prompt, createStreamPrinter()); + + // Update fix count + state.fixCount++; + await saveState(state); + + console.log(`\n\n${COLORS.success}Bug fix attempt complete.${COLORS.reset}`); + console.log(`${COLORS.system}Total fixes attempted: ${state.fixCount}${COLORS.reset}\n`); +} + +/** + * Interactive mode - keep fixing bugs + */ +export async function interactiveMode(session: Session, state: BugFixerState): Promise { + const readline = await import('node:readline'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const askQuestion = (prompt: string): Promise => { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer); + }); + }); + }; + + console.log(`${COLORS.system}Interactive mode. Describe bugs to fix, or type 'quit' to exit.${COLORS.reset}\n`); + + while (true) { + const input = await askQuestion(`${COLORS.agent}Describe the bug > ${COLORS.reset}`); + + if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') { + break; + } + + if (!input.trim()) continue; + + await fixBug(session, state, input); + } + + rl.close(); +} + +/** + * Show status + */ +export async function showStatus(state: BugFixerState): Promise { + console.log('\nšŸ› Bug Fixer Status\n'); + console.log(`Agent: ${state.agentId || '(not created yet)'}`); + if (state.agentId) { + console.log(` → https://app.letta.com/agents/${state.agentId}`); + } + console.log(`Fixes attempted: ${state.fixCount}`); + console.log(''); +} + +/** + * Reset state + */ +export async function reset(): Promise { + if (existsSync(STATE_FILE)) { + const fs = await import('node:fs/promises'); + await fs.unlink(STATE_FILE); + } + console.log('\nšŸ—‘ļø Bug fixer reset. Agent forgotten.\n'); +} diff --git a/examples/bug-fixer/types.ts b/examples/bug-fixer/types.ts new file mode 100644 index 0000000..7b628d5 --- /dev/null +++ b/examples/bug-fixer/types.ts @@ -0,0 +1,16 @@ +/** + * Bug Fixer Types + */ + +export interface BugFixerState { + agentId: string | null; + fixCount: number; +} + +export interface BugFixerConfig { + model: string; +} + +export const DEFAULT_CONFIG: BugFixerConfig = { + model: 'sonnet', +}; diff --git a/examples/file-organizer/cli.ts b/examples/file-organizer/cli.ts new file mode 100755 index 0000000..34c3e84 --- /dev/null +++ b/examples/file-organizer/cli.ts @@ -0,0 +1,120 @@ +#!/usr/bin/env bun + +/** + * File Organizer CLI + * + * Organize files in directories with AI assistance. + * + * Usage: + * bun cli.ts ~/Downloads # Organize Downloads folder + * bun cli.ts . --strategy=type # Organize by file type + * bun cli.ts . --dry-run # Preview without changes + * bun cli.ts # Interactive mode + * bun cli.ts --status # Show agent status + * bun cli.ts --reset # Reset agent + */ + +import { parseArgs } from 'node:util'; +import { + loadState, + saveState, + getOrCreateAgent, + organizeDirectory, + interactiveMode, + showStatus, + reset, +} from './organizer.js'; + +async function main() { + const { values, positionals } = parseArgs({ + args: process.argv.slice(2), + options: { + strategy: { type: 'string', short: 's' }, + 'dry-run': { type: 'boolean', default: false }, + status: { type: 'boolean', default: false }, + reset: { type: 'boolean', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + allowPositionals: true, + }); + + if (values.help) { + printHelp(); + return; + } + + if (values.reset) { + await reset(); + return; + } + + const state = await loadState(); + + if (values.status) { + await showStatus(state); + return; + } + + // Get or create the agent + const agent = await getOrCreateAgent(state); + + // Save agent ID if new + if (!state.agentId && agent.agentId) { + state.agentId = agent.agentId; + await saveState(state); + console.log(`\x1b[90m[Agent: ${agent.agentId}]\x1b[0m`); + console.log(`\x1b[90m[→ https://app.letta.com/agents/${agent.agentId}]\x1b[0m\n`); + } + + if (positionals.length > 0) { + // Organize the specified directory + const targetDir = positionals[0]; + await organizeDirectory(agent, state, targetDir, values.strategy, values['dry-run']); + } else { + // Interactive mode + await interactiveMode(agent, state); + } + + agent.close(); +} + +function printHelp() { + console.log(` +šŸ“ File Organizer + +Organize files in directories with AI assistance. Remembers your preferences. + +USAGE: + bun cli.ts [directory] Organize a directory + bun cli.ts Interactive mode + bun cli.ts --status Show agent status + bun cli.ts --reset Reset agent (forget preferences) + bun cli.ts -h, --help Show this help + +OPTIONS: + -s, --strategy TYPE Organization strategy (type, date, project) + --dry-run Preview changes without moving files + +EXAMPLES: + bun cli.ts ~/Downloads # Organize Downloads + bun cli.ts ~/Documents --strategy=date # Organize by date + bun cli.ts ./messy-folder --dry-run # Preview only + bun cli.ts . # Organize current directory + +STRATEGIES: + type Group by file extension (images/, documents/, code/) + date Group by date (2024/, 2025/ or by month) + project Group by project (inferred from content) + (none) AI decides best approach + +SAFETY: + - Always previews changes before executing + - Never deletes files (only moves) + - Creates directories as needed + +PERSISTENCE: + The agent learns your organizational preferences over time. +`); +} + +main().catch(console.error); diff --git a/examples/file-organizer/organizer.ts b/examples/file-organizer/organizer.ts new file mode 100644 index 0000000..72af7e4 --- /dev/null +++ b/examples/file-organizer/organizer.ts @@ -0,0 +1,258 @@ +/** + * File Organizer Agent + * + * A persistent agent that helps organize files in directories. + * Remembers your organizational preferences. + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createSession, resumeSession, type Session } from '../../src/index.js'; +import { FileOrganizerState, DEFAULT_CONFIG } from './types.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const STATE_FILE = join(__dirname, 'state.json'); + +// ANSI colors +const COLORS = { + agent: '\x1b[33m', // Yellow + system: '\x1b[90m', // Gray + success: '\x1b[32m', // Green + warn: '\x1b[33m', // Yellow + reset: '\x1b[0m', +}; + +const SYSTEM_PROMPT = `You are a file organizer. Your job is to help organize files in directories. + +## Your Capabilities +- List and read files using Bash (ls, find) and Glob +- Move and rename files using Bash (mv) +- Create directories using Bash (mkdir) +- Read file contents to understand what they are + +## Your Approach +1. **Scan the directory** - List all files and understand what's there +2. **Propose organization** - Suggest how to organize (by type, date, project, etc.) +3. **Get approval** - Always ask before making changes +4. **Execute carefully** - Move files, creating directories as needed +5. **Report results** - Show what was moved where + +## Organization Strategies +- By file type (images/, documents/, code/, etc.) +- By date (2024/, 2025/, or by month) +- By project (project-a/, project-b/) +- By status (inbox/, processed/, archive/) + +## Memory +You remember organizational preferences: +- How this user likes things organized +- Directory structures we've used before +- Naming conventions + +## Safety +- Always preview changes before executing +- Never delete files (only move) +- Create backups of any renamed files +- Handle duplicates carefully`; + +/** + * Load state from disk + */ +export async function loadState(): Promise { + if (existsSync(STATE_FILE)) { + const data = await readFile(STATE_FILE, 'utf-8'); + return JSON.parse(data); + } + return { + agentId: null, + organizationCount: 0, + }; +} + +/** + * Save state to disk + */ +export async function saveState(state: FileOrganizerState): Promise { + await writeFile(STATE_FILE, JSON.stringify(state, null, 2)); +} + +/** + * Get or create the file organizer agent + */ +export async function getOrCreateAgent(state: FileOrganizerState): Promise { + if (state.agentId) { + console.log(`${COLORS.system}Resuming file organizer agent...${COLORS.reset}`); + return resumeSession(state.agentId, { + model: DEFAULT_CONFIG.model, + allowedTools: ['Bash', 'Read', 'Glob'], + permissionMode: 'bypassPermissions', + }); + } + + console.log(`${COLORS.system}Creating new file organizer agent...${COLORS.reset}`); + const session = await createSession({ + model: DEFAULT_CONFIG.model, + systemPrompt: SYSTEM_PROMPT, + memory: [ + { + label: 'organization-preferences', + value: `# Organization Preferences + +## Preferred Structure +(Will learn from user's choices) + +## Naming Conventions +(Will learn from existing files) + +## Past Organizations +(History of what we've organized)`, + description: 'User preferences for file organization', + }, + ], + allowedTools: ['Bash', 'Read', 'Glob'], + permissionMode: 'bypassPermissions', + }); + + return session; +} + +/** + * Stream output with color + */ +function createStreamPrinter(): (text: string) => void { + return (text: string) => { + process.stdout.write(`${COLORS.agent}${text}${COLORS.reset}`); + }; +} + +/** + * Send a message and get response + */ +export async function chat( + session: Session, + message: string, + onOutput?: (text: string) => void +): Promise { + await session.send(message); + + let response = ''; + const printer = onOutput || createStreamPrinter(); + + for await (const msg of session.receive()) { + if (msg.type === 'assistant') { + response += msg.content; + printer(msg.content); + } else if (msg.type === 'tool_call') { + console.log(`\n${COLORS.system}[${msg.name}]${COLORS.reset}`); + } else if (msg.type === 'tool_result') { + console.log(`${COLORS.system}[done]${COLORS.reset}\n`); + } + } + + return response; +} + +/** + * Organize a directory + */ +export async function organizeDirectory( + session: Session, + state: FileOrganizerState, + targetDir: string, + strategy?: string, + dryRun: boolean = false +): Promise { + console.log(`\n${COLORS.system}Analyzing directory: ${targetDir}...${COLORS.reset}\n`); + + let prompt = `Please help me organize the files in: ${targetDir} + +Steps: +1. First, scan the directory and list what's there +2. Propose an organization strategy +3. Show me exactly what would be moved where`; + + if (strategy) { + prompt += `\n\nPreferred strategy: ${strategy}`; + } + + if (dryRun) { + prompt += `\n\nāš ļø DRY RUN MODE: Only show what would be done, don't actually move anything.`; + } else { + prompt += `\n\nAfter showing the plan, ask me to confirm before moving anything.`; + } + + await chat(session, prompt, createStreamPrinter()); + + // Update count + state.organizationCount++; + await saveState(state); + + console.log(`\n\n${COLORS.success}Organization complete.${COLORS.reset}`); + console.log(`${COLORS.system}Total organizations: ${state.organizationCount}${COLORS.reset}\n`); +} + +/** + * Interactive mode + */ +export async function interactiveMode(session: Session, state: FileOrganizerState): Promise { + const readline = await import('node:readline'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const askQuestion = (prompt: string): Promise => { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer); + }); + }); + }; + + console.log(`${COLORS.system}Interactive mode. Ask me to organize directories.${COLORS.reset}\n`); + + while (true) { + const input = await askQuestion(`${COLORS.agent}> ${COLORS.reset}`); + + if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') { + break; + } + + if (!input.trim()) continue; + + console.log(''); + await chat(session, input, createStreamPrinter()); + console.log('\n'); + } + + rl.close(); +} + +/** + * Show status + */ +export async function showStatus(state: FileOrganizerState): Promise { + console.log('\nšŸ“ File Organizer Status\n'); + console.log(`Agent: ${state.agentId || '(not created yet)'}`); + if (state.agentId) { + console.log(` → https://app.letta.com/agents/${state.agentId}`); + } + console.log(`Organizations completed: ${state.organizationCount}`); + console.log(''); +} + +/** + * Reset state + */ +export async function reset(): Promise { + if (existsSync(STATE_FILE)) { + const fs = await import('node:fs/promises'); + await fs.unlink(STATE_FILE); + } + console.log('\nšŸ—‘ļø File organizer reset. Agent forgotten.\n'); +} diff --git a/examples/file-organizer/types.ts b/examples/file-organizer/types.ts new file mode 100644 index 0000000..842fa17 --- /dev/null +++ b/examples/file-organizer/types.ts @@ -0,0 +1,16 @@ +/** + * File Organizer Types + */ + +export interface FileOrganizerState { + agentId: string | null; + organizationCount: number; +} + +export interface FileOrganizerConfig { + model: string; +} + +export const DEFAULT_CONFIG: FileOrganizerConfig = { + model: 'sonnet', +}; diff --git a/examples/release-notes/cli.ts b/examples/release-notes/cli.ts new file mode 100755 index 0000000..8e60ee0 --- /dev/null +++ b/examples/release-notes/cli.ts @@ -0,0 +1,122 @@ +#!/usr/bin/env bun + +/** + * Release Notes Generator CLI + * + * Generate release notes from git commits. + * + * Usage: + * bun cli.ts v1.0.0 # Notes from v1.0.0 to HEAD + * bun cli.ts v1.0.0 v1.1.0 # Notes from v1.0.0 to v1.1.0 + * bun cli.ts v1.0.0 -o RELEASE.md # Output to file + * bun cli.ts --status # Show agent status + * bun cli.ts --reset # Reset agent + */ + +import { parseArgs } from 'node:util'; +import { + loadState, + saveState, + getOrCreateAgent, + generateReleaseNotes, + showStatus, + reset, +} from './generator.js'; + +async function main() { + const { values, positionals } = parseArgs({ + args: process.argv.slice(2), + options: { + output: { type: 'string', short: 'o' }, + status: { type: 'boolean', default: false }, + reset: { type: 'boolean', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + allowPositionals: true, + }); + + if (values.help) { + printHelp(); + return; + } + + if (values.reset) { + await reset(); + return; + } + + const state = await loadState(); + + if (values.status) { + await showStatus(state); + return; + } + + if (positionals.length === 0) { + console.log('Error: Please specify a git ref (tag, branch, or commit).\n'); + console.log('Examples:'); + console.log(' bun cli.ts v1.0.0 # From v1.0.0 to HEAD'); + console.log(' bun cli.ts v1.0.0 v1.1.0 # From v1.0.0 to v1.1.0'); + console.log(' bun cli.ts HEAD~10 # Last 10 commits'); + return; + } + + const fromRef = positionals[0]; + const toRef = positionals[1] || 'HEAD'; + + // Get or create the agent + const agent = await getOrCreateAgent(state); + + // Save agent ID if new + if (!state.agentId && agent.agentId) { + state.agentId = agent.agentId; + await saveState(state); + console.log(`\x1b[90m[Agent: ${agent.agentId}]\x1b[0m`); + console.log(`\x1b[90m[→ https://app.letta.com/agents/${agent.agentId}]\x1b[0m\n`); + } + + await generateReleaseNotes(agent, state, fromRef, toRef, values.output); + + agent.close(); +} + +function printHelp() { + console.log(` +šŸ“ Release Notes Generator + +Generate release notes from git commits. Remembers your formatting preferences. + +USAGE: + bun cli.ts [to-ref] Generate notes for commit range + bun cli.ts -o FILE Output to file + bun cli.ts --status Show agent status + bun cli.ts --reset Reset agent (forget preferences) + bun cli.ts -h, --help Show this help + +ARGUMENTS: + from-ref Starting point (tag, branch, commit SHA) + to-ref Ending point (default: HEAD) + +OPTIONS: + -o, --output FILE Write release notes to file + +EXAMPLES: + bun cli.ts v1.0.0 # Changes since v1.0.0 + bun cli.ts v1.0.0 v1.1.0 # Changes between two tags + bun cli.ts HEAD~20 # Last 20 commits + bun cli.ts v1.0.0 -o CHANGELOG.md # Write to file + +CATEGORIES: + The agent automatically categorizes commits: + - šŸš€ Features + - šŸ› Bug Fixes + - šŸ’„ Breaking Changes + - šŸ“š Documentation + - šŸ”§ Maintenance + +PERSISTENCE: + The agent learns your formatting preferences over time. +`); +} + +main().catch(console.error); diff --git a/examples/release-notes/generator.ts b/examples/release-notes/generator.ts new file mode 100644 index 0000000..6145f81 --- /dev/null +++ b/examples/release-notes/generator.ts @@ -0,0 +1,227 @@ +/** + * Release Notes Generator Agent + * + * A persistent agent that generates release notes from git commits. + * Remembers past releases and learns your formatting preferences. + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createSession, resumeSession, type Session } from '../../src/index.js'; +import { ReleaseNotesState, DEFAULT_CONFIG } from './types.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const STATE_FILE = join(__dirname, 'state.json'); + +// ANSI colors +const COLORS = { + agent: '\x1b[35m', // Magenta + system: '\x1b[90m', // Gray + success: '\x1b[32m', // Green + reset: '\x1b[0m', +}; + +const SYSTEM_PROMPT = `You are a release notes generator. Your job is to create clear, useful release notes from git commits. + +## Your Capabilities +- Run git commands to analyze commits +- Read files to understand changes +- Write release notes to files + +## Your Approach +1. **Get commits** - Use git log to get commits in the specified range +2. **Categorize** - Group changes into: + - šŸš€ Features (new functionality) + - šŸ› Bug Fixes + - šŸ’„ Breaking Changes + - šŸ“š Documentation + - šŸ”§ Maintenance/Chores +3. **Summarize** - Write human-readable descriptions, not raw commit messages +4. **Format** - Use clean markdown with consistent style + +## Memory +You remember past releases. Use this to: +- Maintain consistent formatting +- Reference version numbers correctly +- Avoid duplicating content from past releases + +## Output Format +\`\`\`markdown +# Release Notes - vX.Y.Z + +## šŸš€ Features +- Feature description + +## šŸ› Bug Fixes +- Fix description + +## šŸ’„ Breaking Changes +- What changed and migration steps + +## šŸ“š Documentation +- Doc updates + +## šŸ”§ Maintenance +- Chores, refactors, deps +\`\`\` + +Skip empty sections. Be concise but informative.`; + +/** + * Load state from disk + */ +export async function loadState(): Promise { + if (existsSync(STATE_FILE)) { + const data = await readFile(STATE_FILE, 'utf-8'); + return JSON.parse(data); + } + return { + agentId: null, + releasesGenerated: 0, + }; +} + +/** + * Save state to disk + */ +export async function saveState(state: ReleaseNotesState): Promise { + await writeFile(STATE_FILE, JSON.stringify(state, null, 2)); +} + +/** + * Get or create the release notes agent + */ +export async function getOrCreateAgent(state: ReleaseNotesState): Promise { + if (state.agentId) { + console.log(`${COLORS.system}Resuming release notes agent...${COLORS.reset}`); + return resumeSession(state.agentId, { + model: DEFAULT_CONFIG.model, + allowedTools: ['Bash', 'Read', 'Write', 'Glob'], + permissionMode: 'bypassPermissions', + }); + } + + console.log(`${COLORS.system}Creating new release notes agent...${COLORS.reset}`); + const session = await createSession({ + model: DEFAULT_CONFIG.model, + systemPrompt: SYSTEM_PROMPT, + memory: [ + { + label: 'release-history', + value: `# Release History + +## Past Releases +(Releases I've generated) + +## Formatting Preferences +(Learned from feedback) + +## Project Context +(What this project does)`, + description: 'History of releases and formatting preferences', + }, + ], + allowedTools: ['Bash', 'Read', 'Write', 'Glob'], + permissionMode: 'bypassPermissions', + }); + + return session; +} + +/** + * Stream output with color + */ +function createStreamPrinter(): (text: string) => void { + return (text: string) => { + process.stdout.write(`${COLORS.agent}${text}${COLORS.reset}`); + }; +} + +/** + * Send a message and get response + */ +export async function chat( + session: Session, + message: string, + onOutput?: (text: string) => void +): Promise { + await session.send(message); + + let response = ''; + const printer = onOutput || createStreamPrinter(); + + for await (const msg of session.receive()) { + if (msg.type === 'assistant') { + response += msg.content; + printer(msg.content); + } else if (msg.type === 'tool_call') { + console.log(`\n${COLORS.system}[${msg.name}]${COLORS.reset}`); + } else if (msg.type === 'tool_result') { + console.log(`${COLORS.system}[done]${COLORS.reset}\n`); + } + } + + return response; +} + +/** + * Generate release notes + */ +export async function generateReleaseNotes( + session: Session, + state: ReleaseNotesState, + fromRef: string, + toRef: string = 'HEAD', + outputFile?: string +): Promise { + console.log(`\n${COLORS.system}Generating release notes for ${fromRef}..${toRef}...${COLORS.reset}\n`); + + let prompt = `Generate release notes for the commits between ${fromRef} and ${toRef}. + +Steps: +1. Run \`git log ${fromRef}..${toRef} --oneline\` to see the commits +2. If needed, read specific files to understand changes better +3. Categorize and summarize the changes +4. Output clean, formatted release notes`; + + if (outputFile) { + prompt += `\n\nWrite the release notes to: ${outputFile}`; + } + + await chat(session, prompt, createStreamPrinter()); + + // Update count + state.releasesGenerated++; + await saveState(state); + + console.log(`\n\n${COLORS.success}Release notes generated.${COLORS.reset}`); + console.log(`${COLORS.system}Total releases generated: ${state.releasesGenerated}${COLORS.reset}\n`); +} + +/** + * Show status + */ +export async function showStatus(state: ReleaseNotesState): Promise { + console.log('\nšŸ“ Release Notes Generator Status\n'); + console.log(`Agent: ${state.agentId || '(not created yet)'}`); + if (state.agentId) { + console.log(` → https://app.letta.com/agents/${state.agentId}`); + } + console.log(`Releases generated: ${state.releasesGenerated}`); + console.log(''); +} + +/** + * Reset state + */ +export async function reset(): Promise { + if (existsSync(STATE_FILE)) { + const fs = await import('node:fs/promises'); + await fs.unlink(STATE_FILE); + } + console.log('\nšŸ—‘ļø Release notes generator reset. Agent forgotten.\n'); +} diff --git a/examples/release-notes/types.ts b/examples/release-notes/types.ts new file mode 100644 index 0000000..b88a7a8 --- /dev/null +++ b/examples/release-notes/types.ts @@ -0,0 +1,16 @@ +/** + * Release Notes Generator Types + */ + +export interface ReleaseNotesState { + agentId: string | null; + releasesGenerated: number; +} + +export interface ReleaseNotesConfig { + model: string; +} + +export const DEFAULT_CONFIG: ReleaseNotesConfig = { + model: 'sonnet', +};