@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
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Memory Filesystem Status Check
|
|
4
|
+
*
|
|
5
|
+
* Read-only check of the current memFS sync status.
|
|
6
|
+
* Shows conflicts, pending changes, and overall sync health.
|
|
7
|
+
* Analogous to `git status`.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx memfs-status.ts <agent-id>
|
|
11
|
+
*
|
|
12
|
+
* Output: JSON object with sync status
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const Letta = require("@letta-ai/letta-client")
|
|
22
|
+
.default as typeof import("@letta-ai/letta-client").default;
|
|
23
|
+
|
|
24
|
+
function getApiKey(): string {
|
|
25
|
+
if (process.env.LETTA_API_KEY) {
|
|
26
|
+
return process.env.LETTA_API_KEY;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const settingsPath = join(homedir(), ".letta", "settings.json");
|
|
30
|
+
try {
|
|
31
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
32
|
+
if (settings.env?.LETTA_API_KEY) {
|
|
33
|
+
return settings.env.LETTA_API_KEY;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Settings file doesn't exist or is invalid
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(
|
|
40
|
+
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// We can't import checkMemoryFilesystemStatus directly since it relies on
|
|
45
|
+
// getClient() which uses the CLI's auth chain. Instead, we reimplement the
|
|
46
|
+
// status check logic using the standalone client pattern.
|
|
47
|
+
// This keeps the script fully standalone and runnable outside the CLI process.
|
|
48
|
+
|
|
49
|
+
import { createHash } from "node:crypto";
|
|
50
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
51
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
52
|
+
import { relative } from "node:path";
|
|
53
|
+
|
|
54
|
+
const MEMORY_FS_STATE_FILE = ".sync-state.json";
|
|
55
|
+
|
|
56
|
+
type SyncState = {
|
|
57
|
+
systemBlocks: Record<string, string>;
|
|
58
|
+
systemFiles: Record<string, string>;
|
|
59
|
+
detachedBlocks: Record<string, string>;
|
|
60
|
+
detachedFiles: Record<string, string>;
|
|
61
|
+
detachedBlockIds: Record<string, string>;
|
|
62
|
+
lastSync: string | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function hashContent(content: string): string {
|
|
66
|
+
return createHash("sha256").update(content).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getMemoryRoot(agentId: string): string {
|
|
70
|
+
return join(homedir(), ".letta", "agents", agentId, "memory");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadSyncState(agentId: string): SyncState {
|
|
74
|
+
const statePath = join(getMemoryRoot(agentId), MEMORY_FS_STATE_FILE);
|
|
75
|
+
if (!existsSync(statePath)) {
|
|
76
|
+
return {
|
|
77
|
+
systemBlocks: {},
|
|
78
|
+
systemFiles: {},
|
|
79
|
+
detachedBlocks: {},
|
|
80
|
+
detachedFiles: {},
|
|
81
|
+
detachedBlockIds: {},
|
|
82
|
+
lastSync: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const raw = readFileSync(statePath, "utf-8");
|
|
88
|
+
const parsed = JSON.parse(raw) as Partial<SyncState> & {
|
|
89
|
+
blocks?: Record<string, string>;
|
|
90
|
+
files?: Record<string, string>;
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
systemBlocks: parsed.systemBlocks || parsed.blocks || {},
|
|
94
|
+
systemFiles: parsed.systemFiles || parsed.files || {},
|
|
95
|
+
detachedBlocks: parsed.detachedBlocks || {},
|
|
96
|
+
detachedFiles: parsed.detachedFiles || {},
|
|
97
|
+
detachedBlockIds: parsed.detachedBlockIds || {},
|
|
98
|
+
lastSync: parsed.lastSync || null,
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
systemBlocks: {},
|
|
103
|
+
systemFiles: {},
|
|
104
|
+
detachedBlocks: {},
|
|
105
|
+
detachedFiles: {},
|
|
106
|
+
detachedBlockIds: {},
|
|
107
|
+
lastSync: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function scanMdFiles(dir: string, baseDir = dir): Promise<string[]> {
|
|
113
|
+
if (!existsSync(dir)) return [];
|
|
114
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
115
|
+
const results: string[] = [];
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const fullPath = join(dir, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
results.push(...(await scanMdFiles(fullPath, baseDir)));
|
|
120
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
121
|
+
results.push(relative(baseDir, fullPath));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function labelFromPath(relativePath: string): string {
|
|
128
|
+
return relativePath.replace(/\\/g, "/").replace(/\.md$/, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readMemoryFiles(
|
|
132
|
+
dir: string,
|
|
133
|
+
): Promise<Map<string, { content: string }>> {
|
|
134
|
+
const files = await scanMdFiles(dir);
|
|
135
|
+
const entries = new Map<string, { content: string }>();
|
|
136
|
+
for (const rel of files) {
|
|
137
|
+
const label = labelFromPath(rel);
|
|
138
|
+
const content = await readFile(join(dir, rel), "utf-8");
|
|
139
|
+
entries.set(label, { content });
|
|
140
|
+
}
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const MANAGED_LABELS = new Set([
|
|
145
|
+
"memory_filesystem",
|
|
146
|
+
"skills",
|
|
147
|
+
"loaded_skills",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
interface StatusResult {
|
|
151
|
+
conflicts: Array<{ label: string }>;
|
|
152
|
+
pendingFromFile: string[];
|
|
153
|
+
pendingFromBlock: string[];
|
|
154
|
+
newFiles: string[];
|
|
155
|
+
newBlocks: string[];
|
|
156
|
+
isClean: boolean;
|
|
157
|
+
lastSync: string | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function checkStatus(agentId: string): Promise<StatusResult> {
|
|
161
|
+
const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
|
162
|
+
const client = new Letta({ apiKey: getApiKey(), baseUrl });
|
|
163
|
+
|
|
164
|
+
const root = getMemoryRoot(agentId);
|
|
165
|
+
const systemDir = join(root, "system");
|
|
166
|
+
// Detached files go at root level (flat structure)
|
|
167
|
+
const detachedDir = root;
|
|
168
|
+
|
|
169
|
+
// Ensure directories exist
|
|
170
|
+
for (const dir of [root, systemDir]) {
|
|
171
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const systemFiles = await readMemoryFiles(systemDir);
|
|
175
|
+
const detachedFiles = await readMemoryFiles(detachedDir);
|
|
176
|
+
systemFiles.delete("memory_filesystem");
|
|
177
|
+
|
|
178
|
+
// Fetch attached blocks
|
|
179
|
+
const blocksResponse = await client.agents.blocks.list(agentId, {
|
|
180
|
+
limit: 1000,
|
|
181
|
+
});
|
|
182
|
+
const blocks = Array.isArray(blocksResponse)
|
|
183
|
+
? blocksResponse
|
|
184
|
+
: ((blocksResponse as { items?: unknown[] }).items as Array<{
|
|
185
|
+
label?: string;
|
|
186
|
+
value?: string;
|
|
187
|
+
}>) || [];
|
|
188
|
+
|
|
189
|
+
const systemBlockMap = new Map(
|
|
190
|
+
blocks
|
|
191
|
+
.filter((b: { label?: string }) => b.label)
|
|
192
|
+
.map((b: { label?: string; value?: string }) => [
|
|
193
|
+
b.label as string,
|
|
194
|
+
b.value || "",
|
|
195
|
+
]),
|
|
196
|
+
);
|
|
197
|
+
systemBlockMap.delete("memory_filesystem");
|
|
198
|
+
|
|
199
|
+
const lastState = loadSyncState(agentId);
|
|
200
|
+
|
|
201
|
+
const conflicts: Array<{ label: string }> = [];
|
|
202
|
+
const pendingFromFile: string[] = [];
|
|
203
|
+
const pendingFromBlock: string[] = [];
|
|
204
|
+
const newFiles: string[] = [];
|
|
205
|
+
const newBlocks: string[] = [];
|
|
206
|
+
|
|
207
|
+
// Fetch user blocks
|
|
208
|
+
const detachedBlockMap = new Map<string, string>();
|
|
209
|
+
for (const [label, blockId] of Object.entries(lastState.detachedBlockIds)) {
|
|
210
|
+
try {
|
|
211
|
+
const block = await client.blocks.retrieve(blockId);
|
|
212
|
+
detachedBlockMap.set(label, block.value || "");
|
|
213
|
+
} catch {
|
|
214
|
+
// Block no longer exists
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function classify(
|
|
219
|
+
label: string,
|
|
220
|
+
fileContent: string | null,
|
|
221
|
+
blockValue: string | null,
|
|
222
|
+
lastFileHash: string | null,
|
|
223
|
+
lastBlockHash: string | null,
|
|
224
|
+
) {
|
|
225
|
+
const fileHash = fileContent !== null ? hashContent(fileContent) : null;
|
|
226
|
+
const blockHash = blockValue !== null ? hashContent(blockValue) : null;
|
|
227
|
+
const fileChanged = fileHash !== lastFileHash;
|
|
228
|
+
const blockChanged = blockHash !== lastBlockHash;
|
|
229
|
+
|
|
230
|
+
if (fileContent !== null && blockValue === null) {
|
|
231
|
+
if (lastBlockHash && !fileChanged) return;
|
|
232
|
+
newFiles.push(label);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (fileContent === null && blockValue !== null) {
|
|
236
|
+
if (lastFileHash && !blockChanged) return;
|
|
237
|
+
newBlocks.push(label);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (fileContent === null || blockValue === null) return;
|
|
241
|
+
if (fileHash === blockHash) return;
|
|
242
|
+
if (fileChanged && blockChanged) {
|
|
243
|
+
conflicts.push({ label });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (fileChanged && !blockChanged) {
|
|
247
|
+
pendingFromFile.push(label);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!fileChanged && blockChanged) {
|
|
251
|
+
pendingFromBlock.push(label);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check system labels
|
|
256
|
+
const systemLabels = new Set([
|
|
257
|
+
...systemFiles.keys(),
|
|
258
|
+
...systemBlockMap.keys(),
|
|
259
|
+
...Object.keys(lastState.systemBlocks),
|
|
260
|
+
...Object.keys(lastState.systemFiles),
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
for (const label of [...systemLabels].sort()) {
|
|
264
|
+
if (MANAGED_LABELS.has(label)) continue;
|
|
265
|
+
classify(
|
|
266
|
+
label,
|
|
267
|
+
systemFiles.get(label)?.content ?? null,
|
|
268
|
+
systemBlockMap.get(label) ?? null,
|
|
269
|
+
lastState.systemFiles[label] ?? null,
|
|
270
|
+
lastState.systemBlocks[label] ?? null,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check user labels
|
|
275
|
+
const userLabels = new Set([
|
|
276
|
+
...detachedFiles.keys(),
|
|
277
|
+
...detachedBlockMap.keys(),
|
|
278
|
+
...Object.keys(lastState.detachedBlocks),
|
|
279
|
+
...Object.keys(lastState.detachedFiles),
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
for (const label of [...userLabels].sort()) {
|
|
283
|
+
classify(
|
|
284
|
+
label,
|
|
285
|
+
detachedFiles.get(label)?.content ?? null,
|
|
286
|
+
detachedBlockMap.get(label) ?? null,
|
|
287
|
+
lastState.detachedFiles[label] ?? null,
|
|
288
|
+
lastState.detachedBlocks[label] ?? null,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const isClean =
|
|
293
|
+
conflicts.length === 0 &&
|
|
294
|
+
pendingFromFile.length === 0 &&
|
|
295
|
+
pendingFromBlock.length === 0 &&
|
|
296
|
+
newFiles.length === 0 &&
|
|
297
|
+
newBlocks.length === 0;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
conflicts,
|
|
301
|
+
pendingFromFile,
|
|
302
|
+
pendingFromBlock,
|
|
303
|
+
newFiles,
|
|
304
|
+
newBlocks,
|
|
305
|
+
isClean,
|
|
306
|
+
lastSync: lastState.lastSync,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// CLI Entry Point
|
|
311
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
312
|
+
if (isMainModule) {
|
|
313
|
+
const args = process.argv.slice(2);
|
|
314
|
+
|
|
315
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
316
|
+
console.log(`
|
|
317
|
+
Usage: npx tsx memfs-status.ts <agent-id>
|
|
318
|
+
|
|
319
|
+
Shows the current memFS sync status (read-only).
|
|
320
|
+
Analogous to 'git status'.
|
|
321
|
+
|
|
322
|
+
Arguments:
|
|
323
|
+
agent-id Agent ID to check (can use $LETTA_AGENT_ID)
|
|
324
|
+
|
|
325
|
+
Output: JSON object with:
|
|
326
|
+
- conflicts: blocks where both file and block changed
|
|
327
|
+
- pendingFromFile: file changed, block didn't
|
|
328
|
+
- pendingFromBlock: block changed, file didn't
|
|
329
|
+
- newFiles: file exists without a block
|
|
330
|
+
- newBlocks: block exists without a file
|
|
331
|
+
- isClean: true if everything is in sync
|
|
332
|
+
- lastSync: timestamp of last sync
|
|
333
|
+
`);
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const agentId = args[0];
|
|
338
|
+
if (!agentId) {
|
|
339
|
+
console.error("Error: agent-id is required");
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
checkStatus(agentId)
|
|
344
|
+
.then((status) => {
|
|
345
|
+
console.log(JSON.stringify(status, null, 2));
|
|
346
|
+
})
|
|
347
|
+
.catch((error) => {
|
|
348
|
+
console.error(
|
|
349
|
+
"Error checking memFS status:",
|
|
350
|
+
error instanceof Error ? error.message : String(error),
|
|
351
|
+
);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
@@ -74,7 +74,13 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
|
|
|
74
74
|
renderedValue = value.length > 0 ? '' : chalk.inverse(' ');
|
|
75
75
|
let i = 0;
|
|
76
76
|
for (const char of value) {
|
|
77
|
-
|
|
77
|
+
const isCursorPosition = i >= cursorOffset - cursorActualWidth && i <= cursorOffset;
|
|
78
|
+
if (isCursorPosition && char === '\n') {
|
|
79
|
+
// Newline at cursor: show inverted space (visible cursor) then the newline
|
|
80
|
+
renderedValue += chalk.inverse(' ') + char;
|
|
81
|
+
} else {
|
|
82
|
+
renderedValue += isCursorPosition ? chalk.inverse(char) : char;
|
|
83
|
+
}
|
|
78
84
|
i++;
|
|
79
85
|
}
|
|
80
86
|
if (value.length > 0 && cursorOffset === value.length) {
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* Backup Memory Blocks to Local Files
|
|
4
|
-
*
|
|
5
|
-
* Exports all memory blocks from an agent to local files for checkpointing and editing.
|
|
6
|
-
* Creates a timestamped backup directory with:
|
|
7
|
-
* - Individual .md files for each memory block
|
|
8
|
-
* - manifest.json with metadata
|
|
9
|
-
*
|
|
10
|
-
* This script is standalone and can be run outside the CLI process.
|
|
11
|
-
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* npx tsx backup-memory.ts <agent-id> [backup-dir]
|
|
15
|
-
*
|
|
16
|
-
* Example:
|
|
17
|
-
* npx tsx backup-memory.ts agent-abc123
|
|
18
|
-
* npx tsx backup-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
-
import { createRequire } from "node:module";
|
|
23
|
-
import { homedir } from "node:os";
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
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
|
-
export interface BackupManifest {
|
|
34
|
-
agent_id: string;
|
|
35
|
-
timestamp: string;
|
|
36
|
-
backup_path: string;
|
|
37
|
-
blocks: Array<{
|
|
38
|
-
id: string;
|
|
39
|
-
label: string;
|
|
40
|
-
filename: string;
|
|
41
|
-
limit: number;
|
|
42
|
-
value_length: number;
|
|
43
|
-
}>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Get API key from env var or settings file
|
|
48
|
-
*/
|
|
49
|
-
function getApiKey(): string {
|
|
50
|
-
if (process.env.LETTA_API_KEY) {
|
|
51
|
-
return process.env.LETTA_API_KEY;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const settingsPath = join(homedir(), ".letta", "settings.json");
|
|
55
|
-
try {
|
|
56
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
57
|
-
if (settings.env?.LETTA_API_KEY) {
|
|
58
|
-
return settings.env.LETTA_API_KEY;
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Settings file doesn't exist or is invalid
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
throw new Error(
|
|
65
|
-
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Create a Letta client with auth from env/settings
|
|
71
|
-
*/
|
|
72
|
-
function createClient(): LettaClient {
|
|
73
|
-
const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
|
74
|
-
return new Letta({ apiKey: getApiKey(), baseUrl });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Backup memory blocks to local files
|
|
79
|
-
*/
|
|
80
|
-
async function backupMemory(
|
|
81
|
-
agentId: string,
|
|
82
|
-
backupDir?: string,
|
|
83
|
-
): Promise<string> {
|
|
84
|
-
const client = createClient();
|
|
85
|
-
|
|
86
|
-
// Create backup directory
|
|
87
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
88
|
-
const defaultBackupDir = join(
|
|
89
|
-
process.cwd(),
|
|
90
|
-
".letta",
|
|
91
|
-
"backups",
|
|
92
|
-
agentId,
|
|
93
|
-
timestamp,
|
|
94
|
-
);
|
|
95
|
-
const backupPath = backupDir || defaultBackupDir;
|
|
96
|
-
|
|
97
|
-
mkdirSync(backupPath, { recursive: true });
|
|
98
|
-
|
|
99
|
-
console.log(`Backing up memory blocks for agent ${agentId}...`);
|
|
100
|
-
console.log(`Backup location: ${backupPath}`);
|
|
101
|
-
|
|
102
|
-
// Get all memory blocks
|
|
103
|
-
const blocksResponse = await client.agents.blocks.list(agentId);
|
|
104
|
-
const blocks = Array.isArray(blocksResponse)
|
|
105
|
-
? blocksResponse
|
|
106
|
-
: (blocksResponse as { items?: unknown[] }).items ||
|
|
107
|
-
(blocksResponse as { blocks?: unknown[] }).blocks ||
|
|
108
|
-
[];
|
|
109
|
-
|
|
110
|
-
console.log(`Found ${blocks.length} memory blocks`);
|
|
111
|
-
|
|
112
|
-
// Export each block to a file
|
|
113
|
-
const manifest: BackupManifest = {
|
|
114
|
-
agent_id: agentId,
|
|
115
|
-
timestamp: new Date().toISOString(),
|
|
116
|
-
backup_path: backupPath,
|
|
117
|
-
blocks: [],
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
for (const block of blocks as Array<{
|
|
121
|
-
id: string;
|
|
122
|
-
label?: string;
|
|
123
|
-
value?: string;
|
|
124
|
-
limit?: number;
|
|
125
|
-
}>) {
|
|
126
|
-
const label = block.label || `block-${block.id}`;
|
|
127
|
-
// For hierarchical labels like "A/B", create directory A/ with file B.md
|
|
128
|
-
const filename = `${label}.md`;
|
|
129
|
-
const filepath = join(backupPath, filename);
|
|
130
|
-
|
|
131
|
-
// Create parent directories if label contains slashes
|
|
132
|
-
const parentDir = dirname(filepath);
|
|
133
|
-
if (parentDir !== backupPath) {
|
|
134
|
-
mkdirSync(parentDir, { recursive: true });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Write block content to file
|
|
138
|
-
const content = block.value || "";
|
|
139
|
-
writeFileSync(filepath, content, "utf-8");
|
|
140
|
-
|
|
141
|
-
console.log(` ✓ ${label} -> ${filename} (${content.length} chars)`);
|
|
142
|
-
|
|
143
|
-
// Add to manifest
|
|
144
|
-
manifest.blocks.push({
|
|
145
|
-
id: block.id,
|
|
146
|
-
label,
|
|
147
|
-
filename,
|
|
148
|
-
limit: block.limit || 0,
|
|
149
|
-
value_length: content.length,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Write manifest
|
|
154
|
-
const manifestPath = join(backupPath, "manifest.json");
|
|
155
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
156
|
-
console.log(` ✓ manifest.json`);
|
|
157
|
-
|
|
158
|
-
console.log(`\n✅ Backup complete: ${backupPath}`);
|
|
159
|
-
return backupPath;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// CLI Entry Point - check if this file is being run directly
|
|
163
|
-
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
164
|
-
if (isMainModule) {
|
|
165
|
-
const args = process.argv.slice(2);
|
|
166
|
-
|
|
167
|
-
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
168
|
-
console.log(`
|
|
169
|
-
Usage: npx tsx backup-memory.ts <agent-id> [backup-dir]
|
|
170
|
-
|
|
171
|
-
Arguments:
|
|
172
|
-
agent-id Agent ID to backup (can use $LETTA_AGENT_ID)
|
|
173
|
-
backup-dir Optional custom backup directory
|
|
174
|
-
Default: .letta/backups/<agent-id>/<timestamp>
|
|
175
|
-
|
|
176
|
-
Examples:
|
|
177
|
-
npx tsx backup-memory.ts agent-abc123
|
|
178
|
-
npx tsx backup-memory.ts $LETTA_AGENT_ID
|
|
179
|
-
npx tsx backup-memory.ts agent-abc123 .letta/backups/working
|
|
180
|
-
`);
|
|
181
|
-
process.exit(0);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const agentId = args[0];
|
|
185
|
-
const backupDir = args[1];
|
|
186
|
-
|
|
187
|
-
if (!agentId) {
|
|
188
|
-
console.error("Error: agent-id is required");
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
backupMemory(agentId, backupDir)
|
|
193
|
-
.then((path) => {
|
|
194
|
-
// Output just the path for easy capture in scripts
|
|
195
|
-
console.log(path);
|
|
196
|
-
})
|
|
197
|
-
.catch((error) => {
|
|
198
|
-
console.error(
|
|
199
|
-
"Error backing up memory:",
|
|
200
|
-
error instanceof Error ? error.message : String(error),
|
|
201
|
-
);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export { backupMemory };
|