136
.letta/memory-utils/backup-memory.ts
Executable file
136
.letta/memory-utils/backup-memory.ts
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Backup Memory Blocks to Local Files
|
||||
*
|
||||
* Exports all memory blocks from an agent to local files for checkpointing and editing.
|
||||
* Creates a timestamped backup directory with:
|
||||
* - Individual .md files for each memory block
|
||||
* - manifest.json with metadata
|
||||
*
|
||||
* Usage:
|
||||
* bun .letta/memory-utils/backup-memory.ts <agent-id> [backup-dir]
|
||||
*
|
||||
* Example:
|
||||
* bun .letta/memory-utils/backup-memory.ts agent-abc123
|
||||
* bun .letta/memory-utils/backup-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working
|
||||
*/
|
||||
|
||||
import { writeFile, mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { getClient } from "../../src/agent/client";
|
||||
import { settingsManager } from "../../src/settings-manager";
|
||||
|
||||
interface BackupManifest {
|
||||
agent_id: string;
|
||||
timestamp: string;
|
||||
backup_path: string;
|
||||
blocks: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
filename: string;
|
||||
limit: number;
|
||||
value_length: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function backupMemory(agentId: string, backupDir?: string): Promise<string> {
|
||||
await settingsManager.initialize();
|
||||
const client = await getClient();
|
||||
|
||||
// Create backup directory
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const defaultBackupDir = join(process.cwd(), ".letta", "backups", agentId, timestamp);
|
||||
const backupPath = backupDir || defaultBackupDir;
|
||||
|
||||
await mkdir(backupPath, { recursive: true });
|
||||
|
||||
console.log(`Backing up memory blocks for agent ${agentId}...`);
|
||||
console.log(`Backup location: ${backupPath}`);
|
||||
|
||||
// Get all memory blocks
|
||||
const blocksResponse = await client.agents.blocks.list(agentId);
|
||||
const blocks = Array.isArray(blocksResponse)
|
||||
? blocksResponse
|
||||
: (blocksResponse.items || blocksResponse.blocks || []);
|
||||
|
||||
console.log(`Found ${blocks.length} memory blocks`);
|
||||
|
||||
// Export each block to a file
|
||||
const manifest: BackupManifest = {
|
||||
agent_id: agentId,
|
||||
timestamp: new Date().toISOString(),
|
||||
backup_path: backupPath,
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
for (const block of blocks) {
|
||||
const label = block.label || `block-${block.id}`;
|
||||
const filename = `${label}.md`;
|
||||
const filepath = join(backupPath, filename);
|
||||
|
||||
// Write block content to file
|
||||
const content = block.value || "";
|
||||
await writeFile(filepath, content, "utf-8");
|
||||
|
||||
console.log(` ✓ ${label} -> ${filename} (${content.length} chars)`);
|
||||
|
||||
// Add to manifest
|
||||
manifest.blocks.push({
|
||||
id: block.id,
|
||||
label,
|
||||
filename,
|
||||
limit: block.limit || 0,
|
||||
value_length: content.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
const manifestPath = join(backupPath, "manifest.json");
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
||||
console.log(` ✓ manifest.json`);
|
||||
|
||||
console.log(`\n✅ Backup complete: ${backupPath}`);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
// CLI Entry Point
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
||||
console.log(`
|
||||
Usage: bun .letta/memory-utils/backup-memory.ts <agent-id> [backup-dir]
|
||||
|
||||
Arguments:
|
||||
agent-id Agent ID to backup (can use $LETTA_PARENT_AGENT_ID)
|
||||
backup-dir Optional custom backup directory
|
||||
Default: .letta/backups/<agent-id>/<timestamp>
|
||||
|
||||
Examples:
|
||||
bun .letta/memory-utils/backup-memory.ts agent-abc123
|
||||
bun .letta/memory-utils/backup-memory.ts $LETTA_PARENT_AGENT_ID
|
||||
bun .letta/memory-utils/backup-memory.ts agent-abc123 .letta/backups/working
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const agentId = args[0];
|
||||
const backupDir = args[1];
|
||||
|
||||
if (!agentId) {
|
||||
console.error("Error: agent-id is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
backupMemory(agentId, backupDir)
|
||||
.then((path) => {
|
||||
// Output just the path for easy capture in scripts
|
||||
console.log(path);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error backing up memory:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { backupMemory, type BackupManifest };
|
||||
270
.letta/memory-utils/restore-memory.ts
Executable file
270
.letta/memory-utils/restore-memory.ts
Executable file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Restore Memory Blocks from Local Files
|
||||
*
|
||||
* Imports memory blocks from local files back into an agent.
|
||||
* Reads files from a backup directory and updates the agent's memory blocks.
|
||||
*
|
||||
* Usage:
|
||||
* bun .letta/memory-utils/restore-memory.ts <agent-id> <backup-dir>
|
||||
*
|
||||
* Example:
|
||||
* bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working
|
||||
* bun .letta/memory-utils/restore-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working
|
||||
*/
|
||||
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { join, extname } from "node:path";
|
||||
import { getClient } from "../../src/agent/client";
|
||||
import { settingsManager } from "../../src/settings-manager";
|
||||
import type { BackupManifest } from "./backup-memory";
|
||||
|
||||
async function restoreMemory(
|
||||
agentId: string,
|
||||
backupDir: string,
|
||||
options: { dryRun?: boolean } = {},
|
||||
): Promise<void> {
|
||||
await settingsManager.initialize();
|
||||
const client = await getClient();
|
||||
|
||||
console.log(`Restoring memory blocks for agent ${agentId}...`);
|
||||
console.log(`Source: ${backupDir}`);
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log("⚠️ DRY RUN MODE - No changes will be made\n");
|
||||
}
|
||||
|
||||
// Read manifest
|
||||
const manifestPath = join(backupDir, "manifest.json");
|
||||
let manifest: BackupManifest | null = null;
|
||||
|
||||
try {
|
||||
const manifestContent = await readFile(manifestPath, "utf-8");
|
||||
manifest = JSON.parse(manifestContent);
|
||||
console.log(`Loaded manifest (${manifest.blocks.length} blocks)\n`);
|
||||
} catch (error) {
|
||||
console.warn("Warning: No manifest.json found, will scan directory for .md files");
|
||||
}
|
||||
|
||||
// Get current agent blocks
|
||||
const blocksResponse = await client.agents.blocks.list(agentId);
|
||||
const currentBlocks = Array.isArray(blocksResponse)
|
||||
? blocksResponse
|
||||
: (blocksResponse.items || blocksResponse.blocks || []);
|
||||
const blocksByLabel = new Map(currentBlocks.map((b) => [b.label, b]));
|
||||
|
||||
// Determine which files to restore
|
||||
let filesToRestore: Array<{ label: string; filename: string; blockId?: string }> = [];
|
||||
|
||||
if (manifest) {
|
||||
// Use manifest
|
||||
filesToRestore = manifest.blocks.map((b) => ({
|
||||
label: b.label,
|
||||
filename: b.filename,
|
||||
blockId: b.id,
|
||||
}));
|
||||
} else {
|
||||
// Scan directory for .md files
|
||||
const files = await readdir(backupDir);
|
||||
filesToRestore = files
|
||||
.filter((f) => extname(f) === ".md")
|
||||
.map((f) => ({
|
||||
label: f.replace(/\.md$/, ""),
|
||||
filename: f,
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(`Found ${filesToRestore.length} files to restore\n`);
|
||||
|
||||
// Detect blocks to delete (exist on agent but not in backup)
|
||||
const backupLabels = new Set(filesToRestore.map((f) => f.label));
|
||||
const blocksToDelete = currentBlocks.filter((b) => !backupLabels.has(b.label));
|
||||
|
||||
// Restore each block
|
||||
let updated = 0;
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
let deleted = 0;
|
||||
|
||||
// Track new blocks for later confirmation
|
||||
const blocksToCreate: Array<{ label: string; value: string; description: string }> = [];
|
||||
|
||||
for (const { label, filename } of filesToRestore) {
|
||||
const filepath = join(backupDir, filename);
|
||||
|
||||
try {
|
||||
const newValue = await readFile(filepath, "utf-8");
|
||||
const existingBlock = blocksByLabel.get(label);
|
||||
|
||||
if (existingBlock) {
|
||||
// Update existing block
|
||||
const unchanged = existingBlock.value === newValue;
|
||||
|
||||
if (unchanged) {
|
||||
console.log(` ⏭️ ${label} - unchanged, skipping`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
await client.agents.blocks.update(label, {
|
||||
agent_id: agentId,
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
const oldLen = existingBlock.value?.length || 0;
|
||||
const newLen = newValue.length;
|
||||
const diff = newLen - oldLen;
|
||||
const diffStr = diff > 0 ? `+${diff}` : `${diff}`;
|
||||
|
||||
console.log(` ✓ ${label} - updated (${oldLen} -> ${newLen} chars, ${diffStr})`);
|
||||
updated++;
|
||||
} else {
|
||||
// New block - collect for later confirmation
|
||||
console.log(` ➕ ${label} - new block (${newValue.length} chars)`);
|
||||
blocksToCreate.push({
|
||||
label,
|
||||
value: newValue,
|
||||
description: `Memory block: ${label}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(` ❌ ${label} - error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new blocks (exist in backup but not on agent)
|
||||
if (blocksToCreate.length > 0) {
|
||||
console.log(`\n➕ Found ${blocksToCreate.length} new block(s) to create:`);
|
||||
for (const block of blocksToCreate) {
|
||||
console.log(` - ${block.label} (${block.value.length} chars)`);
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
console.log(`\nThese blocks will be CREATED on the agent.`);
|
||||
console.log(`Press Ctrl+C to cancel, or press Enter to confirm creation...`);
|
||||
|
||||
// Wait for user confirmation
|
||||
await new Promise<void>((resolve) => {
|
||||
process.stdin.once('data', () => resolve());
|
||||
});
|
||||
|
||||
console.log();
|
||||
for (const block of blocksToCreate) {
|
||||
try {
|
||||
// Create the block
|
||||
const createdBlock = await client.blocks.create({
|
||||
label: block.label,
|
||||
value: block.value,
|
||||
description: block.description,
|
||||
limit: 20000,
|
||||
});
|
||||
|
||||
if (!createdBlock.id) {
|
||||
throw new Error(`Created block ${block.label} has no ID`);
|
||||
}
|
||||
|
||||
// Attach the newly created block to the agent
|
||||
await client.agents.blocks.attach(createdBlock.id, {
|
||||
agent_id: agentId,
|
||||
});
|
||||
|
||||
console.log(` ✅ ${block.label} - created and attached`);
|
||||
created++;
|
||||
} catch (error) {
|
||||
console.error(` ❌ ${block.label} - error creating: ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`\n(Would create these blocks if not in dry-run mode)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deletions (blocks that exist on agent but not in backup)
|
||||
if (blocksToDelete.length > 0) {
|
||||
console.log(`\n⚠️ Found ${blocksToDelete.length} block(s) that were removed from backup:`);
|
||||
for (const block of blocksToDelete) {
|
||||
console.log(` - ${block.label}`);
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
console.log(`\nThese blocks will be DELETED from the agent.`);
|
||||
console.log(`Press Ctrl+C to cancel, or press Enter to confirm deletion...`);
|
||||
|
||||
// Wait for user confirmation
|
||||
await new Promise<void>((resolve) => {
|
||||
process.stdin.once('data', () => resolve());
|
||||
});
|
||||
|
||||
console.log();
|
||||
for (const block of blocksToDelete) {
|
||||
try {
|
||||
await client.agents.blocks.detach(block.id, {
|
||||
agent_id: agentId,
|
||||
});
|
||||
console.log(` 🗑️ ${block.label} - deleted`);
|
||||
deleted++;
|
||||
} catch (error) {
|
||||
console.error(` ❌ ${block.label} - error deleting: ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`\n(Would delete these blocks if not in dry-run mode)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Summary:`);
|
||||
console.log(` Updated: ${updated}`);
|
||||
console.log(` Skipped: ${skipped}`);
|
||||
console.log(` Created: ${created}`);
|
||||
console.log(` Deleted: ${deleted}`);
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(`\n⚠️ DRY RUN - No changes were made`);
|
||||
console.log(` Run without --dry-run to apply changes`);
|
||||
} else {
|
||||
console.log(`\n✅ Restore complete`);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Entry Point
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
||||
console.log(`
|
||||
Usage: bun .letta/memory-utils/restore-memory.ts <agent-id> <backup-dir> [options]
|
||||
|
||||
Arguments:
|
||||
agent-id Agent ID to restore to (can use $LETTA_PARENT_AGENT_ID)
|
||||
backup-dir Backup directory containing memory block files
|
||||
|
||||
Options:
|
||||
--dry-run Preview changes without applying them
|
||||
|
||||
Examples:
|
||||
bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working
|
||||
bun .letta/memory-utils/restore-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working
|
||||
bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working --dry-run
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const agentId = args[0];
|
||||
const backupDir = args[1];
|
||||
const dryRun = args.includes("--dry-run");
|
||||
|
||||
if (!agentId || !backupDir) {
|
||||
console.error("Error: agent-id and backup-dir are required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
restoreMemory(agentId, backupDir, { dryRun })
|
||||
.catch((error) => {
|
||||
console.error("Error restoring memory:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { restoreMemory };
|
||||
Reference in New Issue
Block a user