feat: add basic utility examples with agent persistence

Three practical examples showing core SDK capabilities:

- **bug-fixer**: Find and fix bugs, remembers codebase patterns
- **release-notes**: Generate release notes from git commits
- **file-organizer**: Organize directories, learns preferences

Each example uses agent persistence (getOrCreateAgent pattern) so the
agent remembers context across sessions - the key Letta differentiator.

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>
This commit is contained in:
Cameron Pfiffer
2026-01-27 15:51:27 -08:00
parent d5bbce6dec
commit 3b4811f035
9 changed files with 1137 additions and 0 deletions

107
examples/bug-fixer/cli.ts Executable file
View File

@@ -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);

255
examples/bug-fixer/fixer.ts Normal file
View File

@@ -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<BugFixerState> {
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<void> {
await writeFile(STATE_FILE, JSON.stringify(state, null, 2));
}
/**
* Get or create the bug fixer agent
*/
export async function getOrCreateAgent(state: BugFixerState): Promise<Session> {
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<string> {
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<void> {
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<void> {
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = (prompt: string): Promise<string> => {
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<void> {
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<void> {
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');
}

View File

@@ -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',
};

120
examples/file-organizer/cli.ts Executable file
View File

@@ -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);

View File

@@ -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<FileOrganizerState> {
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<void> {
await writeFile(STATE_FILE, JSON.stringify(state, null, 2));
}
/**
* Get or create the file organizer agent
*/
export async function getOrCreateAgent(state: FileOrganizerState): Promise<Session> {
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<string> {
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<void> {
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<void> {
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = (prompt: string): Promise<string> => {
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<void> {
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<void> {
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');
}

View File

@@ -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',
};

122
examples/release-notes/cli.ts Executable file
View File

@@ -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 <from-ref> [to-ref] Generate notes for commit range
bun cli.ts <from-ref> -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);

View File

@@ -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<ReleaseNotesState> {
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<void> {
await writeFile(STATE_FILE, JSON.stringify(state, null, 2));
}
/**
* Get or create the release notes agent
*/
export async function getOrCreateAgent(state: ReleaseNotesState): Promise<Session> {
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<string> {
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<void> {
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<void> {
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<void> {
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');
}

View File

@@ -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',
};