From 79c92e92cc23eed700a5668228ae0c27fa87ebbf Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Sun, 11 Jan 2026 19:25:53 -0800 Subject: [PATCH] fix: memory defrag skill location (#518) Co-authored-by: Letta --- .letta/memory-utils/backup-memory.ts | 136 ---------- .../skills/builtin}/memory-defrag/SKILL.md | 63 ++--- .../memory-defrag/scripts/backup-memory.ts | 198 ++++++++++++++ .../memory-defrag/scripts}/restore-memory.ts | 242 ++++++++++++------ 4 files changed, 380 insertions(+), 259 deletions(-) delete mode 100755 .letta/memory-utils/backup-memory.ts rename {.skills => src/skills/builtin}/memory-defrag/SKILL.md (67%) create mode 100644 src/skills/builtin/memory-defrag/scripts/backup-memory.ts rename {.letta/memory-utils => src/skills/builtin/memory-defrag/scripts}/restore-memory.ts (56%) mode change 100755 => 100644 diff --git a/.letta/memory-utils/backup-memory.ts b/.letta/memory-utils/backup-memory.ts deleted file mode 100755 index a3819b2..0000000 --- a/.letta/memory-utils/backup-memory.ts +++ /dev/null @@ -1,136 +0,0 @@ -#!/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 [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 { - 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 [backup-dir] - -Arguments: - agent-id Agent ID to backup (can use $LETTA_PARENT_AGENT_ID) - backup-dir Optional custom backup directory - Default: .letta/backups// - -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 }; diff --git a/.skills/memory-defrag/SKILL.md b/src/skills/builtin/memory-defrag/SKILL.md similarity index 67% rename from .skills/memory-defrag/SKILL.md rename to src/skills/builtin/memory-defrag/SKILL.md index c5bbd7f..5faa9de 100644 --- a/.skills/memory-defrag/SKILL.md +++ b/src/skills/builtin/memory-defrag/SKILL.md @@ -21,18 +21,13 @@ This skill helps you maintain clean, well-organized memory blocks by: ## Workflow -### Step 1: Download Agent File and Dump Memory to Files +### Step 1: Backup Memory to Files ```bash -# Download agent file to backups -bun .letta/memory-utils/download-agent.ts $LETTA_AGENT_ID - -# Dump memory blocks to files -bun .letta/memory-utils/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working +npx tsx /scripts/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working ``` This creates: -- `.letta/backups//.af` - Complete agent file backup for full rollback - `.letta/backups///` - Timestamped memory blocks backup - `.letta/backups/working/` - Working directory with editable files - Each memory block as a `.md` file: `persona.md`, `human.md`, `project.md`, etc. @@ -72,7 +67,7 @@ The memory subagent will: ### Step 3: Restore Cleaned Files to Memory ```bash -bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working +npx tsx /scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working ``` This will: @@ -84,10 +79,10 @@ This will: ## Example Complete Flow ```typescript -// Step 1: Download agent file and dump memory +// Step 1: Backup memory to files Bash({ - command: "bun .letta/memory-utils/download-agent.ts $LETTA_AGENT_ID && bun .letta/memory-utils/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working", - description: "Download agent file and dump memory to files" + command: "npx tsx /scripts/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working", + description: "Backup memory to files" }) // Step 2: Clean up (subagent edits files and deletes merged ones) @@ -99,35 +94,21 @@ Task({ // Step 3: Restore Bash({ - command: "bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working", + command: "npx tsx /scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working", description: "Restore cleaned memory blocks" }) ``` ## Rollback -If something goes wrong, you have two rollback options: - -### Option 1: Restore Memory Blocks Only +If something goes wrong, restore from a previous backup: ```bash # Find the backup directory ls -la .letta/backups// # Restore from specific timestamp -bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups// -``` - -### Option 2: Full Agent Restore (Nuclear Option) - -If memory restoration isn't enough, restore the entire agent from the .af backup: - -```bash -# Find the agent backup -ls -la .letta/backups//*.af - -# The .af file can be used to recreate the agent entirely -# Use: letta --from-af .letta/backups//.af +npx tsx /scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups// ``` ## Dry Run @@ -135,20 +116,20 @@ ls -la .letta/backups//*.af Preview changes without applying them: ```bash -bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run +npx tsx /scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run ``` ## What the Memory Subagent Does The memory subagent focuses on cleaning up files. It: -- ✅ Reads files from `.letta/backups/working/` -- ✅ Edits files to improve structure and consolidate redundancy -- ✅ Merges related blocks together to reduce fragmentation -- ✅ Reorganizes information for better clarity and scannability -- ✅ Deletes source files after merging their content (using Bash `rm` command) -- ✅ Provides detailed before/after reports including merge operations -- ❌ Does NOT run backup scripts (main agent does this) -- ❌ Does NOT run restore scripts (main agent does this) +- Reads files from `.letta/backups/working/` +- Edits files to improve structure and consolidate redundancy +- Merges related blocks together to reduce fragmentation +- Reorganizes information for better clarity and scannability +- Deletes source files after merging their content (using Bash `rm` command) +- Provides detailed before/after reports including merge operations +- Does NOT run backup scripts (main agent does this) +- Does NOT run restore scripts (main agent does this) The memory subagent runs with `bypassPermissions` mode, giving it full Bash access to delete files after merging them. The focus is on consolidation and reorganization. @@ -169,10 +150,10 @@ The memory subagent runs with `bypassPermissions` mode, giving it full Bash acce - After merging blocks, DELETE the source files to avoid duplication **When to DELETE a file:** -- ✅ **After merging** - You've consolidated its content into another block (common and encouraged) -- ✅ **Junk data** - File contains only irrelevant test/junk data with no project connection -- ✅ **Empty/deprecated** - File is just a notice with no unique information -- ❌ **Don't delete** - If file has unique information that hasn't been merged elsewhere +- After merging - You've consolidated its content into another block (common and encouraged) +- Junk data - File contains only irrelevant test/junk data with no project connection +- Empty/deprecated - File is just a notice with no unique information +- Don't delete - If file has unique information that hasn't been merged elsewhere **What to preserve:** - User preferences (sacred - never delete) diff --git a/src/skills/builtin/memory-defrag/scripts/backup-memory.ts b/src/skills/builtin/memory-defrag/scripts/backup-memory.ts new file mode 100644 index 0000000..486b09d --- /dev/null +++ b/src/skills/builtin/memory-defrag/scripts/backup-memory.ts @@ -0,0 +1,198 @@ +#!/usr/bin/env npx tsx +/** + * 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 + * + * This script is standalone and can be run outside the CLI process. + * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json. + * + * Usage: + * npx tsx backup-memory.ts [backup-dir] + * + * Example: + * npx tsx backup-memory.ts agent-abc123 + * npx tsx backup-memory.ts $LETTA_AGENT_ID .letta/backups/working + */ + +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; + +export interface BackupManifest { + agent_id: string; + timestamp: string; + backup_path: string; + blocks: Array<{ + id: string; + label: string; + filename: string; + limit: number; + value_length: number; + }>; +} + +/** + * Get API key from env var or settings file + */ +function getApiKey(): string { + if (process.env.LETTA_API_KEY) { + return process.env.LETTA_API_KEY; + } + + const settingsPath = join(homedir(), ".letta", "settings.json"); + try { + const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); + if (settings.env?.LETTA_API_KEY) { + return settings.env.LETTA_API_KEY; + } + } catch { + // Settings file doesn't exist or is invalid + } + + throw new Error( + "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.", + ); +} + +/** + * Create a Letta client with auth from env/settings + */ +function createClient(): LettaClient { + return new Letta({ apiKey: getApiKey() }); +} + +/** + * Backup memory blocks to local files + */ +async function backupMemory( + agentId: string, + backupDir?: string, +): Promise { + const client = createClient(); + + // Create backup directory + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const defaultBackupDir = join( + process.cwd(), + ".letta", + "backups", + agentId, + timestamp, + ); + const backupPath = backupDir || defaultBackupDir; + + mkdirSync(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 as { items?: unknown[] }).items || + (blocksResponse as { blocks?: unknown[] }).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 as Array<{ + id: string; + label?: string; + value?: string; + limit?: number; + }>) { + 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 || ""; + writeFileSync(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"); + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); + console.log(` ✓ manifest.json`); + + console.log(`\n✅ Backup complete: ${backupPath}`); + return backupPath; +} + +// CLI Entry Point - check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + const args = process.argv.slice(2); + + if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + console.log(` +Usage: npx tsx backup-memory.ts [backup-dir] + +Arguments: + agent-id Agent ID to backup (can use $LETTA_AGENT_ID) + backup-dir Optional custom backup directory + Default: .letta/backups// + +Examples: + npx tsx backup-memory.ts agent-abc123 + npx tsx backup-memory.ts $LETTA_AGENT_ID + npx tsx 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 instanceof Error ? error.message : String(error), + ); + process.exit(1); + }); +} + +export { backupMemory }; diff --git a/.letta/memory-utils/restore-memory.ts b/src/skills/builtin/memory-defrag/scripts/restore-memory.ts old mode 100755 new mode 100644 similarity index 56% rename from .letta/memory-utils/restore-memory.ts rename to src/skills/builtin/memory-defrag/scripts/restore-memory.ts index bc26567..089704d --- a/.letta/memory-utils/restore-memory.ts +++ b/src/skills/builtin/memory-defrag/scripts/restore-memory.ts @@ -1,61 +1,116 @@ -#!/usr/bin/env bun +#!/usr/bin/env npx tsx /** * 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. - * + * + * This script is standalone and can be run outside the CLI process. + * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json. + * * Usage: - * bun .letta/memory-utils/restore-memory.ts - * + * npx tsx restore-memory.ts [options] + * * 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 + * npx tsx restore-memory.ts agent-abc123 .letta/backups/working + * npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run */ -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 { readdirSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { extname, join } from "node:path"; + import type { BackupManifest } from "./backup-memory"; +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; + +/** + * Get API key from env var or settings file + */ +function getApiKey(): string { + if (process.env.LETTA_API_KEY) { + return process.env.LETTA_API_KEY; + } + + const settingsPath = join(homedir(), ".letta", "settings.json"); + try { + const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); + if (settings.env?.LETTA_API_KEY) { + return settings.env.LETTA_API_KEY; + } + } catch { + // Settings file doesn't exist or is invalid + } + + throw new Error( + "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.", + ); +} + +/** + * Create a Letta client with auth from env/settings + */ +function createClient(): LettaClient { + return new Letta({ apiKey: getApiKey() }); +} + +/** + * Restore memory blocks from local files + */ async function restoreMemory( agentId: string, backupDir: string, options: { dryRun?: boolean } = {}, ): Promise { - await settingsManager.initialize(); - const client = await getClient(); - + const client = createClient(); + 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"); + const manifestContent = readFileSync(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"); + console.log(`Loaded manifest (${manifest?.blocks.length} blocks)\n`); + } catch { + 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])); - + const currentBlocks = Array.isArray(blocksResponse) + ? blocksResponse + : (blocksResponse as { items?: unknown[] }).items || + (blocksResponse as { blocks?: unknown[] }).blocks || + []; + const blocksByLabel = new Map( + (currentBlocks as Array<{ label: string; id: string; value?: string }>).map( + (b) => [b.label, b], + ), + ); + // Determine which files to restore - let filesToRestore: Array<{ label: string; filename: string; blockId?: string }> = []; - + let filesToRestore: Array<{ + label: string; + filename: string; + blockId?: string; + }> = []; + if (manifest) { // Use manifest filesToRestore = manifest.blocks.map((b) => ({ @@ -65,7 +120,7 @@ async function restoreMemory( })); } else { // Scan directory for .md files - const files = await readdir(backupDir); + const files = readdirSync(backupDir); filesToRestore = files .filter((f) => extname(f) === ".md") .map((f) => ({ @@ -73,52 +128,60 @@ async function restoreMemory( 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)); - + const blocksToDelete = ( + currentBlocks as Array<{ label: string; id: string }> + ).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 }> = []; - + 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 newValue = readFileSync(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})`); + + console.log( + ` ✓ ${label} - updated (${oldLen} -> ${newLen} chars, ${diffStr})`, + ); updated++; } else { // New block - collect for later confirmation @@ -130,26 +193,30 @@ async function restoreMemory( }); } } catch (error) { - console.error(` ❌ ${label} - error: ${error.message}`); + console.error( + ` ❌ ${label} - error: ${error instanceof Error ? error.message : String(error)}`, + ); } } - + // 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...`); - + console.log( + `Press Ctrl+C to cancel, or press Enter to confirm creation...`, + ); + // Wait for user confirmation await new Promise((resolve) => { - process.stdin.once('data', () => resolve()); + process.stdin.once("data", () => resolve()); }); - + console.log(); for (const block of blocksToCreate) { try { @@ -160,43 +227,49 @@ async function restoreMemory( 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}`); + console.error( + ` ❌ ${block.label} - error creating: ${error instanceof Error ? error.message : String(error)}`, + ); } } } 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:`); + 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...`); - + console.log( + `Press Ctrl+C to cancel, or press Enter to confirm deletion...`, + ); + // Wait for user confirmation await new Promise((resolve) => { - process.stdin.once('data', () => resolve()); + process.stdin.once("data", () => resolve()); }); - + console.log(); for (const block of blocksToDelete) { try { @@ -206,20 +279,22 @@ async function restoreMemory( console.log(` 🗑️ ${block.label} - deleted`); deleted++; } catch (error) { - console.error(` ❌ ${block.label} - error deleting: ${error.message}`); + console.error( + ` ❌ ${block.label} - error deleting: ${error instanceof Error ? error.message : String(error)}`, + ); } } } 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`); @@ -228,43 +303,46 @@ async function restoreMemory( } } -// CLI Entry Point -if (import.meta.main) { +// CLI Entry Point - check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { 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 [options] +Usage: npx tsx restore-memory.ts [options] Arguments: - agent-id Agent ID to restore to (can use $LETTA_PARENT_AGENT_ID) + agent-id Agent ID to restore to (can use $LETTA_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 + npx tsx restore-memory.ts agent-abc123 .letta/backups/working + npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working + npx tsx 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); - }); + + restoreMemory(agentId, backupDir, { dryRun }).catch((error) => { + console.error( + "Error restoring memory:", + error instanceof Error ? error.message : String(error), + ); + process.exit(1); + }); } export { restoreMemory };