Agent skill
coding-patterns
Architecture and design patterns for lettactl. Reference when building new features, displays, or commands.
Install this agent skill to your Project
npx add-skill https://github.com/nouamanecodes/lettactl/tree/main/.skills/coding-patterns
SKILL.md
Display Architecture
All UX/display code lives in src/lib/ux/display/. Commands never format output directly — they transform data into typed interfaces and call display functions.
Entry List Pattern
For any list of entries shown chronologically (messages, archival memory, etc.), use the shared entry-list.ts module:
import { displayEntryList, EntryListItem } from './entry-list';
const items: EntryListItem[] = data.map(d => ({
metaLine: chalk.dim(d.timestamp) + purple(` [${d.label}]`),
content: d.text,
}));
return displayEntryList('Title (N)', items, optionalNote);
metaLine: pre-formatted metadata (timestamp, role, tags, score, etc.)content: text body — truncated for summary views, full for--fullviews- The module handles fancy/plain switching internally via
shouldUseFancyUx()
Used by: messages.ts, archival.ts
Box Pattern
For detail views and structured data (describe command output), use box.ts:
import { createBox, createBoxWithRows, BoxRow } from '../box';
// Key-value box
const rows: BoxRow[] = [
{ key: 'ID', value: data.id },
{ key: 'Name', value: data.name },
];
lines.push(...createBox('Title', rows, width));
// Free-form rows box
const items = data.map(d => chalk.white(d.name));
lines.push(...createBoxWithRows('Section (N)', items, width));
Used by: details.ts (describe command)
Table Pattern
For resource listings (get agents, get blocks, etc.), use the table helpers in resources.ts:
- Header row with column names
- Status dot + padded columns
- Fancy mode uses colored box frame, plain mode uses dashes
Used by: resources.ts (get command list views)
When to Use Which
| View Type | Pattern | Example |
|---|---|---|
| Chronological entries | Entry List | messages, archival memory |
| Resource listing | Table | get agents, get blocks, get tools |
| Single resource detail | Box | describe agent, describe block |
| Full content dump | Entry List (--full) |
get archival --full |
Display Module Structure
Every display file follows the same structure:
- Export a typed data interface (e.g.,
AgentData,ArchivalEntryData) - Export a display function that checks
shouldUseFancyUx() - Fancy variant uses chalk colors, box drawing, purple branding
- Plain variant uses simple text — no colors, no box chars
New display modules go in src/lib/ux/display/ with a re-export in index.ts.
Command → Display Flow
Command (get.ts, describe.ts)
→ Fetches raw data from LettaClientWrapper
→ Transforms to typed display interface
→ Calls OutputFormatter adapter (output-formatter.ts)
→ Calls display function (display/*.ts)
→ Returns formatted string
→ output() to stdout
Commands never import chalk or format strings. OutputFormatter adapters transform raw SDK responses to typed display data.
LettaClientWrapper
All SDK calls go through src/lib/letta-client.ts. Never call the SDK directly from commands. Wrapper methods handle:
- Normalizing response shapes
- Consistent error surfaces
- Single place to update when SDK changes
Command Directory Structure
Each command lives in its own directory under src/commands/<command>/:
src/commands/
├── get/
│ ├── index.ts # Re-exports, router logic
│ ├── types.ts # Interfaces, constants (SUPPORTED_RESOURCES, etc.)
│ ├── agents.ts # Handler for `get agents`
│ ├── blocks.ts # Handler for `get blocks`
│ └── ... # One file per resource type
├── delete/
│ ├── index.ts # Router + re-exports (deleteAgentWithCleanup for SDK)
│ ├── types.ts # DELETE_SUPPORTED_RESOURCES, options interfaces
│ ├── agent.ts # Single agent deletion
│ ├── mcp-server.ts # Single MCP server deletion
│ ├── all-agents.ts # Bulk delete handlers
│ └── ...
└── messages/
├── index.ts # Re-exports all commands
├── types.ts # ListOptions, SendOptions, etc.
├── list.ts # listMessagesCommand
├── send.ts # sendMessageCommand
├── utils.ts # Shared helpers (getMessageContent, formatElapsedTime)
└── ...
Directory Contents
| File | Purpose |
|---|---|
index.ts |
Re-exports public API, router logic if needed |
types.ts |
Interfaces, option types, constants like SUPPORTED_RESOURCES |
<handler>.ts |
One file per subcommand or resource type |
utils.ts |
Shared helpers used by multiple handlers |
Router Pattern (index.ts)
For commands with multiple subcommands (get, describe, delete), the index.ts acts as a router:
// src/commands/get/index.ts
import { getAgents } from './agents';
import { getBlocks } from './blocks';
import { SUPPORTED_RESOURCES } from './types';
export default async function getCommand(resource: string, name: string, options: GetOptions, command: any) {
if (!SUPPORTED_RESOURCES.includes(resource)) {
throw new Error(`Unsupported resource: ${resource}`);
}
switch (resource) {
case 'agents': return getAgents(name, options, command);
case 'blocks': return getBlocks(name, options, command);
// ...
}
}
Simple Command Pattern
For single-function commands (health, context, export), keep it simple:
// src/commands/health/index.ts
export { healthCommand } from './health';
// src/commands/health/health.ts
export async function healthCommand(options: HealthOptions, command: any) {
// implementation
}
Re-exporting for SDK
When a function needs to be available to the SDK (like deleteAgentWithCleanup), re-export it from index.ts:
// src/commands/delete/index.ts
export { deleteAgentWithCleanup } from './agent';
File Organization Rules
- Commands (
src/commands/<cmd>/): one directory per command, router + handlers - Display (
src/lib/ux/display/): all formatting, one file per domain - Client (
src/lib/letta-client.ts): all SDK calls - Resolver (
src/lib/agent-resolver.ts): agent name → ID resolution - Shared logic (
src/lib/): validators, error handling, resource utilities
Flags Convention
--no-ux: plain output, no colors/boxes (CI/CD mode)--no-spinner: disable loading spinners-o json: JSON output (bypasses all display formatting)--full: show full content instead of truncated (entry list views)--short: truncate content more aggressively (block content views)--query <text>: semantic search (archival memory)
Didn't find tool you were looking for?