@letta-ai/letta-code 0.14.4 → 0.14.6

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.
@@ -1,506 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * Memory Filesystem Conflict Resolver
4
- *
5
- * Resolves all memFS sync conflicts in a single stateless call.
6
- * The agent provides all resolutions up front as JSON.
7
- * Analogous to `git merge` / `git checkout --theirs/--ours`.
8
- *
9
- * Usage:
10
- * npx tsx memfs-resolve.ts <agent-id> --resolutions '<JSON>'
11
- *
12
- * Example:
13
- * npx tsx memfs-resolve.ts $LETTA_AGENT_ID --resolutions '[{"label":"persona/soul","resolution":"block"},{"label":"human/prefs","resolution":"file"}]'
14
- *
15
- * Resolution options per conflict:
16
- * "file" — Overwrite the memory block with the file contents
17
- * "block" — Overwrite the file with the memory block contents
18
- */
19
-
20
- import { createHash } from "node:crypto";
21
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
- import { readdir, readFile } from "node:fs/promises";
23
- import { createRequire } from "node:module";
24
- import { homedir } from "node:os";
25
- import { dirname, join, relative } from "node:path";
26
- import { parseFrontmatter, READ_ONLY_LABELS } from "./lib/frontmatter";
27
-
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
-
32
- function getApiKey(): string {
33
- if (process.env.LETTA_API_KEY) {
34
- return process.env.LETTA_API_KEY;
35
- }
36
-
37
- const settingsPath = join(homedir(), ".letta", "settings.json");
38
- try {
39
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
40
- if (settings.env?.LETTA_API_KEY) {
41
- return settings.env.LETTA_API_KEY;
42
- }
43
- } catch {
44
- // Settings file doesn't exist or is invalid
45
- }
46
-
47
- throw new Error(
48
- "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
49
- );
50
- }
51
-
52
- interface Resolution {
53
- label: string;
54
- resolution: "file" | "block";
55
- }
56
-
57
- const MEMORY_FS_STATE_FILE = ".sync-state.json";
58
-
59
- // Unified sync state format (matches main memoryFilesystem.ts)
60
- type SyncState = {
61
- blockHashes: Record<string, string>;
62
- fileHashes: Record<string, string>;
63
- blockIds: Record<string, string>;
64
- lastSync: string | null;
65
- };
66
-
67
- function hashContent(content: string): string {
68
- return createHash("sha256").update(content).digest("hex");
69
- }
70
-
71
- // parseFrontmatter provided by shared helper
72
-
73
- /**
74
- * Parse block update from file content (update-mode: only update metadata if present in frontmatter).
75
- */
76
- function parseBlockUpdateFromFileContent(
77
- fileContent: string,
78
- defaultLabel: string,
79
- ): {
80
- label: string;
81
- value: string;
82
- description?: string;
83
- limit?: number;
84
- read_only?: boolean;
85
- hasDescription: boolean;
86
- hasLimit: boolean;
87
- hasReadOnly: boolean;
88
- } {
89
- const { frontmatter, body } = parseFrontmatter(fileContent);
90
- const label = frontmatter.label || defaultLabel;
91
- const hasDescription = Object.hasOwn(frontmatter, "description");
92
- const hasLimit = Object.hasOwn(frontmatter, "limit");
93
- const hasReadOnly = Object.hasOwn(frontmatter, "read_only");
94
-
95
- let limit: number | undefined;
96
- if (hasLimit && frontmatter.limit) {
97
- const parsed = parseInt(frontmatter.limit, 10);
98
- if (!Number.isNaN(parsed) && parsed > 0) {
99
- limit = parsed;
100
- }
101
- }
102
-
103
- return {
104
- label,
105
- value: body,
106
- ...(hasDescription && { description: frontmatter.description }),
107
- ...(hasLimit && limit !== undefined && { limit }),
108
- ...(hasReadOnly && { read_only: frontmatter.read_only === "true" }),
109
- hasDescription,
110
- hasLimit,
111
- hasReadOnly,
112
- };
113
- }
114
-
115
- /**
116
- * Render block to file content with frontmatter.
117
- */
118
- function renderBlockToFileContent(block: {
119
- value?: string | null;
120
- description?: string | null;
121
- limit?: number | null;
122
- read_only?: boolean | null;
123
- }): string {
124
- const lines: string[] = ["---"];
125
- if (block.description) {
126
- // Escape quotes in description
127
- const escaped = block.description.replace(/"/g, '\\"');
128
- lines.push(`description: "${escaped}"`);
129
- }
130
- if (block.limit) {
131
- lines.push(`limit: ${block.limit}`);
132
- }
133
- if (block.read_only === true) {
134
- lines.push("read_only: true");
135
- }
136
- lines.push("---", "", block.value || "");
137
- return lines.join("\n");
138
- }
139
-
140
- function getMemoryRoot(agentId: string): string {
141
- return join(homedir(), ".letta", "agents", agentId, "memory");
142
- }
143
-
144
- function saveSyncState(state: SyncState, agentId: string): void {
145
- const statePath = join(getMemoryRoot(agentId), MEMORY_FS_STATE_FILE);
146
- writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
147
- }
148
-
149
- async function scanMdFiles(
150
- dir: string,
151
- baseDir = dir,
152
- excludeDirs: string[] = [],
153
- ): Promise<string[]> {
154
- if (!existsSync(dir)) return [];
155
- const entries = await readdir(dir, { withFileTypes: true });
156
- const results: string[] = [];
157
- for (const entry of entries) {
158
- const fullPath = join(dir, entry.name);
159
- if (entry.isDirectory()) {
160
- if (excludeDirs.includes(entry.name)) continue;
161
- results.push(...(await scanMdFiles(fullPath, baseDir, excludeDirs)));
162
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
163
- results.push(relative(baseDir, fullPath));
164
- }
165
- }
166
- return results;
167
- }
168
-
169
- function labelFromPath(relativePath: string): string {
170
- return relativePath.replace(/\\/g, "/").replace(/\.md$/, "");
171
- }
172
-
173
- async function readMemoryFiles(
174
- dir: string,
175
- excludeDirs: string[] = [],
176
- ): Promise<Map<string, { content: string }>> {
177
- const files = await scanMdFiles(dir, dir, excludeDirs);
178
- const entries = new Map<string, { content: string }>();
179
- for (const rel of files) {
180
- const label = labelFromPath(rel);
181
- const content = await readFile(join(dir, rel), "utf-8");
182
- entries.set(label, { content });
183
- }
184
- return entries;
185
- }
186
-
187
- function writeMemoryFile(dir: string, label: string, content: string): void {
188
- const filePath = join(dir, `${label}.md`);
189
- const parent = dirname(filePath);
190
- if (!existsSync(parent)) {
191
- mkdirSync(parent, { recursive: true });
192
- }
193
- writeFileSync(filePath, content, "utf-8");
194
- }
195
-
196
- interface ResolveResult {
197
- resolved: Array<{ label: string; resolution: string; action: string }>;
198
- errors: Array<{ label: string; error: string }>;
199
- }
200
-
201
- async function resolveConflicts(
202
- agentId: string,
203
- resolutions: Resolution[],
204
- ): Promise<ResolveResult> {
205
- const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
206
- const client = new Letta({ apiKey: getApiKey(), baseUrl });
207
-
208
- const root = getMemoryRoot(agentId);
209
- const systemDir = join(root, "system");
210
- const detachedDir = root;
211
-
212
- for (const dir of [root, systemDir]) {
213
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
214
- }
215
-
216
- // Read current state
217
- const systemFiles = await readMemoryFiles(systemDir);
218
- const detachedFiles = await readMemoryFiles(detachedDir, ["system", "user"]);
219
-
220
- // Fetch attached blocks
221
- const blocksResponse = await client.agents.blocks.list(agentId, {
222
- limit: 1000,
223
- });
224
- const attachedBlocks = Array.isArray(blocksResponse)
225
- ? blocksResponse
226
- : ((blocksResponse as { items?: unknown[] }).items as Array<{
227
- id?: string;
228
- label?: string;
229
- value?: string;
230
- description?: string | null;
231
- limit?: number | null;
232
- read_only?: boolean;
233
- }>) || [];
234
-
235
- const systemBlockMap = new Map<
236
- string,
237
- {
238
- id: string;
239
- value: string;
240
- description?: string | null;
241
- limit?: number | null;
242
- read_only?: boolean;
243
- }
244
- >();
245
- for (const block of attachedBlocks) {
246
- if (block.label && block.id) {
247
- systemBlockMap.set(block.label, {
248
- id: block.id,
249
- value: block.value || "",
250
- description: block.description,
251
- limit: block.limit,
252
- read_only: block.read_only,
253
- });
254
- }
255
- }
256
-
257
- // Fetch detached blocks via owner tag
258
- const ownedBlocksResponse = await client.blocks.list({
259
- tags: [`owner:${agentId}`],
260
- limit: 1000,
261
- });
262
- const ownedBlocks = Array.isArray(ownedBlocksResponse)
263
- ? ownedBlocksResponse
264
- : ((ownedBlocksResponse as { items?: unknown[] }).items as Array<{
265
- id?: string;
266
- label?: string;
267
- value?: string;
268
- description?: string | null;
269
- limit?: number | null;
270
- read_only?: boolean;
271
- }>) || [];
272
-
273
- const attachedIds = new Set(attachedBlocks.map((b) => b.id));
274
- const detachedBlockMap = new Map<
275
- string,
276
- {
277
- id: string;
278
- value: string;
279
- description?: string | null;
280
- limit?: number | null;
281
- read_only?: boolean;
282
- }
283
- >();
284
- for (const block of ownedBlocks) {
285
- if (block.label && block.id && !attachedIds.has(block.id)) {
286
- if (!systemBlockMap.has(block.label)) {
287
- detachedBlockMap.set(block.label, {
288
- id: block.id,
289
- value: block.value || "",
290
- description: block.description,
291
- limit: block.limit,
292
- read_only: block.read_only,
293
- });
294
- }
295
- }
296
- }
297
-
298
- const result: ResolveResult = { resolved: [], errors: [] };
299
-
300
- for (const { label, resolution } of resolutions) {
301
- try {
302
- // Check system blocks/files first, then detached blocks/files
303
- const systemBlock = systemBlockMap.get(label);
304
- const systemFile = systemFiles.get(label);
305
- const detachedBlock = detachedBlockMap.get(label);
306
- const detachedFile = detachedFiles.get(label);
307
-
308
- const block = systemBlock || detachedBlock;
309
- const file = systemFile || detachedFile;
310
- const dir =
311
- systemBlock || systemFile
312
- ? systemDir
313
- : detachedBlock || detachedFile
314
- ? detachedDir
315
- : null;
316
-
317
- if (!block || !file || !dir) {
318
- result.errors.push({
319
- label,
320
- error: `Could not find both block and file for label "${label}"`,
321
- });
322
- continue;
323
- }
324
-
325
- const effectiveReadOnly =
326
- !!block.read_only || READ_ONLY_LABELS.has(label);
327
-
328
- if (resolution === "file") {
329
- // read_only blocks: ignore local edits, overwrite file from API
330
- if (effectiveReadOnly) {
331
- const fileContent = renderBlockToFileContent(block);
332
- writeMemoryFile(dir, label, fileContent);
333
- result.resolved.push({
334
- label,
335
- resolution: "block",
336
- action: "read_only: kept API version (file overwritten)",
337
- });
338
- continue;
339
- }
340
-
341
- // Use update-mode parsing (only update metadata if present in frontmatter)
342
- const parsed = parseBlockUpdateFromFileContent(file.content, label);
343
- const updatePayload: Record<string, unknown> = { value: parsed.value };
344
- if (parsed.hasDescription)
345
- updatePayload.description = parsed.description;
346
- if (parsed.hasLimit) updatePayload.limit = parsed.limit;
347
- if (parsed.hasReadOnly) updatePayload.read_only = parsed.read_only;
348
- // For detached blocks, also update label if changed
349
- if (!systemBlock) updatePayload.label = label;
350
-
351
- await client.blocks.update(block.id, updatePayload);
352
- result.resolved.push({
353
- label,
354
- resolution: "file",
355
- action: "Updated block from file",
356
- });
357
- } else if (resolution === "block") {
358
- // Overwrite file with block content (including frontmatter)
359
- const fileContent = renderBlockToFileContent(block);
360
- writeMemoryFile(dir, label, fileContent);
361
- result.resolved.push({
362
- label,
363
- resolution: "block",
364
- action: "Updated file from block",
365
- });
366
- }
367
- } catch (error) {
368
- result.errors.push({
369
- label,
370
- error: error instanceof Error ? error.message : String(error),
371
- });
372
- }
373
- }
374
-
375
- // Rebuild sync state in unified format
376
- const updatedSystemFiles = await readMemoryFiles(systemDir);
377
- const updatedDetachedFiles = await readMemoryFiles(detachedDir, [
378
- "system",
379
- "user",
380
- ]);
381
-
382
- // Re-fetch all owned blocks
383
- const updatedOwnedResp = await client.blocks.list({
384
- tags: [`owner:${agentId}`],
385
- limit: 1000,
386
- });
387
- const updatedOwnedBlocks = Array.isArray(updatedOwnedResp)
388
- ? updatedOwnedResp
389
- : ((updatedOwnedResp as { items?: unknown[] }).items as Array<{
390
- id?: string;
391
- label?: string;
392
- value?: string;
393
- }>) || [];
394
-
395
- const blockHashes: Record<string, string> = {};
396
- const blockIds: Record<string, string> = {};
397
- for (const b of updatedOwnedBlocks) {
398
- if (b.label && b.id) {
399
- blockHashes[b.label] = hashContent(b.value || "");
400
- blockIds[b.label] = b.id;
401
- }
402
- }
403
-
404
- const fileHashes: Record<string, string> = {};
405
- for (const [lbl, f] of updatedSystemFiles) {
406
- fileHashes[lbl] = hashContent(f.content);
407
- }
408
- for (const [lbl, f] of updatedDetachedFiles) {
409
- fileHashes[lbl] = hashContent(f.content);
410
- }
411
-
412
- saveSyncState(
413
- {
414
- blockHashes,
415
- fileHashes,
416
- blockIds,
417
- lastSync: new Date().toISOString(),
418
- },
419
- agentId,
420
- );
421
-
422
- return result;
423
- }
424
-
425
- // CLI Entry Point
426
- const isMainModule = import.meta.url === `file://${process.argv[1]}`;
427
- if (isMainModule) {
428
- const args = process.argv.slice(2);
429
-
430
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
431
- console.log(`
432
- Usage: npx tsx memfs-resolve.ts <agent-id> --resolutions '<JSON>'
433
-
434
- Resolves all memory filesystem sync conflicts in one call.
435
- Analogous to 'git merge' with explicit resolution choices.
436
-
437
- Arguments:
438
- agent-id Agent ID (can use $LETTA_AGENT_ID)
439
- --resolutions JSON array of resolutions
440
-
441
- Resolution format:
442
- [{"label": "persona/soul", "resolution": "block"}, {"label": "human/prefs", "resolution": "file"}]
443
-
444
- Resolution options:
445
- "file" — Overwrite the memory block with the file contents
446
- "block" — Overwrite the file with the memory block contents
447
-
448
- Note: read_only blocks always resolve to "block" (API is authoritative).
449
-
450
- Example:
451
- npx tsx memfs-resolve.ts $LETTA_AGENT_ID --resolutions '[{"label":"persona/soul","resolution":"block"}]'
452
- `);
453
- process.exit(0);
454
- }
455
-
456
- const agentId = args[0];
457
- if (!agentId) {
458
- console.error("Error: agent-id is required");
459
- process.exit(1);
460
- }
461
-
462
- // Parse --resolutions flag
463
- const resolutionsIdx = args.indexOf("--resolutions");
464
- if (resolutionsIdx === -1 || resolutionsIdx + 1 >= args.length) {
465
- console.error("Error: --resolutions '<JSON>' is required");
466
- process.exit(1);
467
- }
468
-
469
- let resolutions: Resolution[];
470
- try {
471
- resolutions = JSON.parse(args[resolutionsIdx + 1]);
472
- if (!Array.isArray(resolutions)) {
473
- throw new Error("Resolutions must be a JSON array");
474
- }
475
- for (const r of resolutions) {
476
- if (!r.label || !r.resolution) {
477
- throw new Error(
478
- `Each resolution must have "label" and "resolution" fields`,
479
- );
480
- }
481
- if (r.resolution !== "file" && r.resolution !== "block") {
482
- throw new Error(
483
- `Resolution must be "file" or "block", got "${r.resolution}"`,
484
- );
485
- }
486
- }
487
- } catch (error) {
488
- console.error(
489
- "Error parsing resolutions:",
490
- error instanceof Error ? error.message : String(error),
491
- );
492
- process.exit(1);
493
- }
494
-
495
- resolveConflicts(agentId, resolutions)
496
- .then((res) => {
497
- console.log(JSON.stringify(res, null, 2));
498
- })
499
- .catch((error) => {
500
- console.error(
501
- "Error resolving conflicts:",
502
- error instanceof Error ? error.message : String(error),
503
- );
504
- process.exit(1);
505
- });
506
- }