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:
107
examples/bug-fixer/cli.ts
Executable file
107
examples/bug-fixer/cli.ts
Executable 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
255
examples/bug-fixer/fixer.ts
Normal 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');
|
||||
}
|
||||
16
examples/bug-fixer/types.ts
Normal file
16
examples/bug-fixer/types.ts
Normal 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
120
examples/file-organizer/cli.ts
Executable 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);
|
||||
258
examples/file-organizer/organizer.ts
Normal file
258
examples/file-organizer/organizer.ts
Normal 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');
|
||||
}
|
||||
16
examples/file-organizer/types.ts
Normal file
16
examples/file-organizer/types.ts
Normal 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
122
examples/release-notes/cli.ts
Executable 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);
|
||||
227
examples/release-notes/generator.ts
Normal file
227
examples/release-notes/generator.ts
Normal 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');
|
||||
}
|
||||
16
examples/release-notes/types.ts
Normal file
16
examples/release-notes/types.ts
Normal 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',
|
||||
};
|
||||
Reference in New Issue
Block a user