@letta-ai/letta-code 0.12.5 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Restore Memory Blocks from Local Files
4
+ *
5
+ * Imports memory blocks from local files back into an agent.
6
+ * Reads files from a backup directory and updates the agent's memory blocks.
7
+ *
8
+ * This script is standalone and can be run outside the CLI process.
9
+ * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
10
+ *
11
+ * Usage:
12
+ * npx tsx restore-memory.ts <agent-id> <backup-dir> [options]
13
+ *
14
+ * Example:
15
+ * npx tsx restore-memory.ts agent-abc123 .letta/backups/working
16
+ * npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run
17
+ */
18
+
19
+ import { readdirSync, readFileSync } from "node:fs";
20
+ import { createRequire } from "node:module";
21
+ import { homedir } from "node:os";
22
+ import { extname, join } from "node:path";
23
+
24
+ import type { BackupManifest } from "./backup-memory";
25
+
26
+ // Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
27
+ // (ES module imports don't respect NODE_PATH, but require does)
28
+ const require = createRequire(import.meta.url);
29
+ const Letta = require("@letta-ai/letta-client")
30
+ .default as typeof import("@letta-ai/letta-client").default;
31
+ type LettaClient = InstanceType<typeof Letta>;
32
+
33
+ /**
34
+ * Get API key from env var or settings file
35
+ */
36
+ function getApiKey(): string {
37
+ if (process.env.LETTA_API_KEY) {
38
+ return process.env.LETTA_API_KEY;
39
+ }
40
+
41
+ const settingsPath = join(homedir(), ".letta", "settings.json");
42
+ try {
43
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
44
+ if (settings.env?.LETTA_API_KEY) {
45
+ return settings.env.LETTA_API_KEY;
46
+ }
47
+ } catch {
48
+ // Settings file doesn't exist or is invalid
49
+ }
50
+
51
+ throw new Error(
52
+ "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Create a Letta client with auth from env/settings
58
+ */
59
+ function createClient(): LettaClient {
60
+ return new Letta({ apiKey: getApiKey() });
61
+ }
62
+
63
+ /**
64
+ * Restore memory blocks from local files
65
+ */
66
+ async function restoreMemory(
67
+ agentId: string,
68
+ backupDir: string,
69
+ options: { dryRun?: boolean } = {},
70
+ ): Promise<void> {
71
+ const client = createClient();
72
+
73
+ console.log(`Restoring memory blocks for agent ${agentId}...`);
74
+ console.log(`Source: ${backupDir}`);
75
+
76
+ if (options.dryRun) {
77
+ console.log("⚠️ DRY RUN MODE - No changes will be made\n");
78
+ }
79
+
80
+ // Read manifest
81
+ const manifestPath = join(backupDir, "manifest.json");
82
+ let manifest: BackupManifest | null = null;
83
+
84
+ try {
85
+ const manifestContent = readFileSync(manifestPath, "utf-8");
86
+ manifest = JSON.parse(manifestContent);
87
+ console.log(`Loaded manifest (${manifest?.blocks.length} blocks)\n`);
88
+ } catch {
89
+ console.warn(
90
+ "Warning: No manifest.json found, will scan directory for .md files",
91
+ );
92
+ }
93
+
94
+ // Get current agent blocks
95
+ const blocksResponse = await client.agents.blocks.list(agentId);
96
+ const currentBlocks = Array.isArray(blocksResponse)
97
+ ? blocksResponse
98
+ : (blocksResponse as { items?: unknown[] }).items ||
99
+ (blocksResponse as { blocks?: unknown[] }).blocks ||
100
+ [];
101
+ const blocksByLabel = new Map(
102
+ (currentBlocks as Array<{ label: string; id: string; value?: string }>).map(
103
+ (b) => [b.label, b],
104
+ ),
105
+ );
106
+
107
+ // Determine which files to restore
108
+ let filesToRestore: Array<{
109
+ label: string;
110
+ filename: string;
111
+ blockId?: string;
112
+ }> = [];
113
+
114
+ if (manifest) {
115
+ // Use manifest
116
+ filesToRestore = manifest.blocks.map((b) => ({
117
+ label: b.label,
118
+ filename: b.filename,
119
+ blockId: b.id,
120
+ }));
121
+ } else {
122
+ // Scan directory for .md files
123
+ const files = readdirSync(backupDir);
124
+ filesToRestore = files
125
+ .filter((f) => extname(f) === ".md")
126
+ .map((f) => ({
127
+ label: f.replace(/\.md$/, ""),
128
+ filename: f,
129
+ }));
130
+ }
131
+
132
+ console.log(`Found ${filesToRestore.length} files to restore\n`);
133
+
134
+ // Detect blocks to delete (exist on agent but not in backup)
135
+ const backupLabels = new Set(filesToRestore.map((f) => f.label));
136
+ const blocksToDelete = (
137
+ currentBlocks as Array<{ label: string; id: string }>
138
+ ).filter((b) => !backupLabels.has(b.label));
139
+
140
+ // Restore each block
141
+ let updated = 0;
142
+ let created = 0;
143
+ let skipped = 0;
144
+ let deleted = 0;
145
+
146
+ // Track new blocks for later confirmation
147
+ const blocksToCreate: Array<{
148
+ label: string;
149
+ value: string;
150
+ description: string;
151
+ }> = [];
152
+
153
+ for (const { label, filename } of filesToRestore) {
154
+ const filepath = join(backupDir, filename);
155
+
156
+ try {
157
+ const newValue = readFileSync(filepath, "utf-8");
158
+ const existingBlock = blocksByLabel.get(label);
159
+
160
+ if (existingBlock) {
161
+ // Update existing block
162
+ const unchanged = existingBlock.value === newValue;
163
+
164
+ if (unchanged) {
165
+ console.log(` ⏭️ ${label} - unchanged, skipping`);
166
+ skipped++;
167
+ continue;
168
+ }
169
+
170
+ if (!options.dryRun) {
171
+ await client.agents.blocks.update(label, {
172
+ agent_id: agentId,
173
+ value: newValue,
174
+ });
175
+ }
176
+
177
+ const oldLen = existingBlock.value?.length || 0;
178
+ const newLen = newValue.length;
179
+ const diff = newLen - oldLen;
180
+ const diffStr = diff > 0 ? `+${diff}` : `${diff}`;
181
+
182
+ console.log(
183
+ ` ✓ ${label} - updated (${oldLen} -> ${newLen} chars, ${diffStr})`,
184
+ );
185
+ updated++;
186
+ } else {
187
+ // New block - collect for later confirmation
188
+ console.log(` ➕ ${label} - new block (${newValue.length} chars)`);
189
+ blocksToCreate.push({
190
+ label,
191
+ value: newValue,
192
+ description: `Memory block: ${label}`,
193
+ });
194
+ }
195
+ } catch (error) {
196
+ console.error(
197
+ ` ❌ ${label} - error: ${error instanceof Error ? error.message : String(error)}`,
198
+ );
199
+ }
200
+ }
201
+
202
+ // Handle new blocks (exist in backup but not on agent)
203
+ if (blocksToCreate.length > 0) {
204
+ console.log(`\n➕ Found ${blocksToCreate.length} new block(s) to create:`);
205
+ for (const block of blocksToCreate) {
206
+ console.log(` - ${block.label} (${block.value.length} chars)`);
207
+ }
208
+
209
+ if (!options.dryRun) {
210
+ console.log(`\nThese blocks will be CREATED on the agent.`);
211
+ console.log(
212
+ `Press Ctrl+C to cancel, or press Enter to confirm creation...`,
213
+ );
214
+
215
+ // Wait for user confirmation
216
+ await new Promise<void>((resolve) => {
217
+ process.stdin.once("data", () => resolve());
218
+ });
219
+
220
+ console.log();
221
+ for (const block of blocksToCreate) {
222
+ try {
223
+ // Create the block
224
+ const createdBlock = await client.blocks.create({
225
+ label: block.label,
226
+ value: block.value,
227
+ description: block.description,
228
+ limit: 20000,
229
+ });
230
+
231
+ if (!createdBlock.id) {
232
+ throw new Error(`Created block ${block.label} has no ID`);
233
+ }
234
+
235
+ // Attach the newly created block to the agent
236
+ await client.agents.blocks.attach(createdBlock.id, {
237
+ agent_id: agentId,
238
+ });
239
+
240
+ console.log(` ✅ ${block.label} - created and attached`);
241
+ created++;
242
+ } catch (error) {
243
+ console.error(
244
+ ` ❌ ${block.label} - error creating: ${error instanceof Error ? error.message : String(error)}`,
245
+ );
246
+ }
247
+ }
248
+ } else {
249
+ console.log(`\n(Would create these blocks if not in dry-run mode)`);
250
+ }
251
+ }
252
+
253
+ // Handle deletions (blocks that exist on agent but not in backup)
254
+ if (blocksToDelete.length > 0) {
255
+ console.log(
256
+ `\n⚠️ Found ${blocksToDelete.length} block(s) that were removed from backup:`,
257
+ );
258
+ for (const block of blocksToDelete) {
259
+ console.log(` - ${block.label}`);
260
+ }
261
+
262
+ if (!options.dryRun) {
263
+ console.log(`\nThese blocks will be DELETED from the agent.`);
264
+ console.log(
265
+ `Press Ctrl+C to cancel, or press Enter to confirm deletion...`,
266
+ );
267
+
268
+ // Wait for user confirmation
269
+ await new Promise<void>((resolve) => {
270
+ process.stdin.once("data", () => resolve());
271
+ });
272
+
273
+ console.log();
274
+ for (const block of blocksToDelete) {
275
+ try {
276
+ await client.agents.blocks.detach(block.id, {
277
+ agent_id: agentId,
278
+ });
279
+ console.log(` 🗑️ ${block.label} - deleted`);
280
+ deleted++;
281
+ } catch (error) {
282
+ console.error(
283
+ ` ❌ ${block.label} - error deleting: ${error instanceof Error ? error.message : String(error)}`,
284
+ );
285
+ }
286
+ }
287
+ } else {
288
+ console.log(`\n(Would delete these blocks if not in dry-run mode)`);
289
+ }
290
+ }
291
+
292
+ console.log(`\n📊 Summary:`);
293
+ console.log(` Updated: ${updated}`);
294
+ console.log(` Skipped: ${skipped}`);
295
+ console.log(` Created: ${created}`);
296
+ console.log(` Deleted: ${deleted}`);
297
+
298
+ if (options.dryRun) {
299
+ console.log(`\n⚠️ DRY RUN - No changes were made`);
300
+ console.log(` Run without --dry-run to apply changes`);
301
+ } else {
302
+ console.log(`\n✅ Restore complete`);
303
+ }
304
+ }
305
+
306
+ // CLI Entry Point - check if this file is being run directly
307
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
308
+ if (isMainModule) {
309
+ const args = process.argv.slice(2);
310
+
311
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
312
+ console.log(`
313
+ Usage: npx tsx restore-memory.ts <agent-id> <backup-dir> [options]
314
+
315
+ Arguments:
316
+ agent-id Agent ID to restore to (can use $LETTA_AGENT_ID)
317
+ backup-dir Backup directory containing memory block files
318
+
319
+ Options:
320
+ --dry-run Preview changes without applying them
321
+
322
+ Examples:
323
+ npx tsx restore-memory.ts agent-abc123 .letta/backups/working
324
+ npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working
325
+ npx tsx restore-memory.ts agent-abc123 .letta/backups/working --dry-run
326
+ `);
327
+ process.exit(0);
328
+ }
329
+
330
+ const agentId = args[0];
331
+ const backupDir = args[1];
332
+ const dryRun = args.includes("--dry-run");
333
+
334
+ if (!agentId || !backupDir) {
335
+ console.error("Error: agent-id and backup-dir are required");
336
+ process.exit(1);
337
+ }
338
+
339
+ restoreMemory(agentId, backupDir, { dryRun }).catch((error) => {
340
+ console.error(
341
+ "Error restoring memory:",
342
+ error instanceof Error ? error.message : String(error),
343
+ );
344
+ process.exit(1);
345
+ });
346
+ }
347
+
348
+ export { restoreMemory };