@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 };
|