@letta-ai/letta-code 0.13.10 → 0.14.0
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.
- package/README.md +3 -1
- package/dist/types/protocol.d.ts +1 -1
- package/dist/types/protocol.d.ts.map +1 -1
- package/letta.js +54835 -51696
- package/package.json +2 -2
- package/skills/defragmenting-memory/SKILL.md +86 -80
- package/skills/initializing-memory/SKILL.md +349 -30
- package/skills/syncing-memory-filesystem/SKILL.md +100 -0
- package/skills/syncing-memory-filesystem/scripts/memfs-diff.ts +361 -0
- package/skills/syncing-memory-filesystem/scripts/memfs-resolve.ts +412 -0
- package/skills/syncing-memory-filesystem/scripts/memfs-status.ts +354 -0
- package/vendor/ink-text-input/build/index.js +7 -1
- package/skills/defragmenting-memory/scripts/backup-memory.ts +0 -206
- package/skills/defragmenting-memory/scripts/restore-memory.ts +0 -330
|
@@ -1,330 +0,0 @@
|
|
|
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, statSync } from "node:fs";
|
|
20
|
-
import { createRequire } from "node:module";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
import { extname, join, relative } 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
|
-
const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
|
61
|
-
return new Letta({ apiKey: getApiKey(), baseUrl });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Recursively scan directory for .md files
|
|
66
|
-
* Returns array of relative file paths from baseDir
|
|
67
|
-
*/
|
|
68
|
-
function scanMdFiles(dir: string, baseDir: string = dir): string[] {
|
|
69
|
-
const results: string[] = [];
|
|
70
|
-
const entries = readdirSync(dir);
|
|
71
|
-
|
|
72
|
-
for (const entry of entries) {
|
|
73
|
-
const fullPath = join(dir, entry);
|
|
74
|
-
const stat = statSync(fullPath);
|
|
75
|
-
|
|
76
|
-
if (stat.isDirectory()) {
|
|
77
|
-
// Recursively scan subdirectory
|
|
78
|
-
results.push(...scanMdFiles(fullPath, baseDir));
|
|
79
|
-
} else if (stat.isFile() && extname(entry) === ".md") {
|
|
80
|
-
// Convert to relative path from baseDir
|
|
81
|
-
const relativePath = relative(baseDir, fullPath);
|
|
82
|
-
results.push(relativePath);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return results;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Restore memory blocks from local files
|
|
91
|
-
*/
|
|
92
|
-
async function restoreMemory(
|
|
93
|
-
agentId: string,
|
|
94
|
-
backupDir: string,
|
|
95
|
-
options: { dryRun?: boolean } = {},
|
|
96
|
-
): Promise<void> {
|
|
97
|
-
const client = createClient();
|
|
98
|
-
|
|
99
|
-
console.log(`Restoring memory blocks for agent ${agentId}...`);
|
|
100
|
-
console.log(`Source: ${backupDir}`);
|
|
101
|
-
|
|
102
|
-
if (options.dryRun) {
|
|
103
|
-
console.log("⚠️ DRY RUN MODE - No changes will be made\n");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Read manifest for metadata only (block IDs)
|
|
107
|
-
const manifestPath = join(backupDir, "manifest.json");
|
|
108
|
-
let manifest: BackupManifest | null = null;
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const manifestContent = readFileSync(manifestPath, "utf-8");
|
|
112
|
-
manifest = JSON.parse(manifestContent);
|
|
113
|
-
} catch {
|
|
114
|
-
// Manifest is optional
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Get current agent blocks using direct fetch (SDK may hit wrong server)
|
|
118
|
-
const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
|
119
|
-
const blocksResp = await fetch(
|
|
120
|
-
`${baseUrl}/v1/agents/${agentId}/core-memory`,
|
|
121
|
-
{
|
|
122
|
-
headers: { Authorization: `Bearer ${getApiKey()}` },
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
if (!blocksResp.ok) {
|
|
126
|
-
throw new Error(`Failed to list blocks: ${blocksResp.status}`);
|
|
127
|
-
}
|
|
128
|
-
const blocksJson = (await blocksResp.json()) as { blocks: unknown[] };
|
|
129
|
-
const blocksResponse = blocksJson.blocks;
|
|
130
|
-
const currentBlocks = Array.isArray(blocksResponse)
|
|
131
|
-
? blocksResponse
|
|
132
|
-
: (blocksResponse as { items?: unknown[] }).items ||
|
|
133
|
-
(blocksResponse as { blocks?: unknown[] }).blocks ||
|
|
134
|
-
[];
|
|
135
|
-
const blocksByLabel = new Map(
|
|
136
|
-
(currentBlocks as Array<{ label: string; id: string; value?: string }>).map(
|
|
137
|
-
(b) => [b.label, b],
|
|
138
|
-
),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// Always scan directory for .md files (manifest is only used for block IDs)
|
|
142
|
-
const files = scanMdFiles(backupDir);
|
|
143
|
-
console.log(`Scanned ${files.length} .md files\n`);
|
|
144
|
-
const filesToRestore = files.map((relativePath) => {
|
|
145
|
-
// Convert path like "A/B.md" to label "A/B"
|
|
146
|
-
// Replace backslashes with forward slashes (Windows compatibility)
|
|
147
|
-
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
148
|
-
const label = normalizedPath.replace(/\.md$/, "");
|
|
149
|
-
// Look up block ID from manifest if available
|
|
150
|
-
const manifestBlock = manifest?.blocks.find((b) => b.label === label);
|
|
151
|
-
return {
|
|
152
|
-
label,
|
|
153
|
-
filename: relativePath,
|
|
154
|
-
blockId: manifestBlock?.id,
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Detect blocks to delete (exist on agent but not in backup)
|
|
159
|
-
const backupLabels = new Set(filesToRestore.map((f) => f.label));
|
|
160
|
-
const blocksToDelete = (
|
|
161
|
-
currentBlocks as Array<{ label: string; id: string }>
|
|
162
|
-
).filter((b) => !backupLabels.has(b.label));
|
|
163
|
-
|
|
164
|
-
// Restore each block
|
|
165
|
-
let updated = 0;
|
|
166
|
-
let created = 0;
|
|
167
|
-
let deleted = 0;
|
|
168
|
-
|
|
169
|
-
for (const { label, filename } of filesToRestore) {
|
|
170
|
-
const filepath = join(backupDir, filename);
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const newValue = readFileSync(filepath, "utf-8");
|
|
174
|
-
const existingBlock = blocksByLabel.get(label);
|
|
175
|
-
|
|
176
|
-
if (existingBlock) {
|
|
177
|
-
// Update existing block using block ID (not label, which may contain /)
|
|
178
|
-
if (!options.dryRun) {
|
|
179
|
-
const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
|
180
|
-
const url = `${baseUrl}/v1/blocks/${existingBlock.id}`;
|
|
181
|
-
const resp = await fetch(url, {
|
|
182
|
-
method: "PATCH",
|
|
183
|
-
headers: {
|
|
184
|
-
"Content-Type": "application/json",
|
|
185
|
-
Authorization: `Bearer ${getApiKey()}`,
|
|
186
|
-
},
|
|
187
|
-
body: JSON.stringify({ value: newValue }),
|
|
188
|
-
});
|
|
189
|
-
if (!resp.ok) {
|
|
190
|
-
throw new Error(`${resp.status} ${await resp.text()}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const oldLen = existingBlock.value?.length || 0;
|
|
195
|
-
const newLen = newValue.length;
|
|
196
|
-
const unchanged = existingBlock.value === newValue;
|
|
197
|
-
|
|
198
|
-
if (unchanged) {
|
|
199
|
-
console.log(` ✓ ${label} - restored (${newLen} chars, unchanged)`);
|
|
200
|
-
} else {
|
|
201
|
-
const diff = newLen - oldLen;
|
|
202
|
-
const diffStr = diff > 0 ? `+${diff}` : `${diff}`;
|
|
203
|
-
console.log(
|
|
204
|
-
` ✓ ${label} - restored (${oldLen} -> ${newLen} chars, ${diffStr})`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
updated++;
|
|
208
|
-
} else {
|
|
209
|
-
// New block - create immediately
|
|
210
|
-
if (!options.dryRun) {
|
|
211
|
-
const createdBlock = await client.blocks.create({
|
|
212
|
-
label,
|
|
213
|
-
value: newValue,
|
|
214
|
-
description: `Memory block: ${label}`,
|
|
215
|
-
limit: 20000,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (!createdBlock.id) {
|
|
219
|
-
throw new Error(`Created block ${label} has no ID`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
await client.agents.blocks.attach(createdBlock.id, {
|
|
223
|
-
agent_id: agentId,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
console.log(` ✓ ${label} - created (${newValue.length} chars)`);
|
|
227
|
-
created++;
|
|
228
|
-
}
|
|
229
|
-
} catch (error) {
|
|
230
|
-
console.error(
|
|
231
|
-
` ❌ ${label} - error: ${error instanceof Error ? error.message : String(error)}`,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Handle deletions (blocks that exist on agent but not in backup)
|
|
237
|
-
if (blocksToDelete.length > 0) {
|
|
238
|
-
console.log(
|
|
239
|
-
`\n⚠️ Found ${blocksToDelete.length} block(s) that were removed from backup:`,
|
|
240
|
-
);
|
|
241
|
-
for (const block of blocksToDelete) {
|
|
242
|
-
console.log(` - ${block.label}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!options.dryRun) {
|
|
246
|
-
console.log(`\nThese blocks will be DELETED from the agent.`);
|
|
247
|
-
console.log(
|
|
248
|
-
`Press Ctrl+C to cancel, or press Enter to confirm deletion...`,
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
// Wait for user confirmation
|
|
252
|
-
await new Promise<void>((resolve) => {
|
|
253
|
-
process.stdin.once("data", () => resolve());
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
console.log();
|
|
257
|
-
for (const block of blocksToDelete) {
|
|
258
|
-
try {
|
|
259
|
-
await client.agents.blocks.detach(block.id, {
|
|
260
|
-
agent_id: agentId,
|
|
261
|
-
});
|
|
262
|
-
console.log(` 🗑️ ${block.label} - deleted`);
|
|
263
|
-
deleted++;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
console.error(
|
|
266
|
-
` ❌ ${block.label} - error deleting: ${error instanceof Error ? error.message : String(error)}`,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
271
|
-
console.log(`\n(Would delete these blocks if not in dry-run mode)`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
console.log(`\n📊 Summary:`);
|
|
276
|
-
console.log(` Restored: ${updated}`);
|
|
277
|
-
console.log(` Created: ${created}`);
|
|
278
|
-
console.log(` Deleted: ${deleted}`);
|
|
279
|
-
|
|
280
|
-
if (options.dryRun) {
|
|
281
|
-
console.log(`\n⚠️ DRY RUN - No changes were made`);
|
|
282
|
-
console.log(` Run without --dry-run to apply changes`);
|
|
283
|
-
} else {
|
|
284
|
-
console.log(`\n✅ Restore complete`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// CLI Entry Point - check if this file is being run directly
|
|
289
|
-
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
290
|
-
if (isMainModule) {
|
|
291
|
-
const args = process.argv.slice(2);
|
|
292
|
-
|
|
293
|
-
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
294
|
-
console.log(`
|
|
295
|
-
Usage: npx tsx restore-memory.ts <agent-id> <backup-dir> [options]
|
|
296
|
-
|
|
297
|
-
Arguments:
|
|
298
|
-
agent-id Agent ID to restore to (can use $LETTA_AGENT_ID)
|
|
299
|
-
backup-dir Backup directory containing memory block files
|
|
300
|
-
|
|
301
|
-
Options:
|
|
302
|
-
--dry-run Preview changes without applying them
|
|
303
|
-
|
|
304
|
-
Examples:
|
|
305
|
-
npx tsx restore-memory.ts agent-abc123 .letta/backups/working
|
|
306
|
-
npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
|
307
|
-
npx tsx restore-memory.ts agent-abc123 .letta/backups/working --dry-run
|
|
308
|
-
`);
|
|
309
|
-
process.exit(0);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const agentId = args[0];
|
|
313
|
-
const backupDir = args[1];
|
|
314
|
-
const dryRun = args.includes("--dry-run");
|
|
315
|
-
|
|
316
|
-
if (!agentId || !backupDir) {
|
|
317
|
-
console.error("Error: agent-id and backup-dir are required");
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
restoreMemory(agentId, backupDir, { dryRun }).catch((error) => {
|
|
322
|
-
console.error(
|
|
323
|
-
"Error restoring memory:",
|
|
324
|
-
error instanceof Error ? error.message : String(error),
|
|
325
|
-
);
|
|
326
|
-
process.exit(1);
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export { restoreMemory };
|