@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,391 +0,0 @@
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 { createHash } from "node:crypto";
16
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
17
- import { readdir, readFile } from "node:fs/promises";
18
- import { createRequire } from "node:module";
19
- import { homedir } from "node:os";
20
- import { join, relative } from "node:path";
21
- import { hashFileBody, READ_ONLY_LABELS } from "./lib/frontmatter";
22
-
23
- const require = createRequire(import.meta.url);
24
- const Letta = require("@letta-ai/letta-client")
25
- .default as typeof import("@letta-ai/letta-client").default;
26
-
27
- function getApiKey(): string {
28
- if (process.env.LETTA_API_KEY) {
29
- return process.env.LETTA_API_KEY;
30
- }
31
-
32
- const settingsPath = join(homedir(), ".letta", "settings.json");
33
- try {
34
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
35
- if (settings.env?.LETTA_API_KEY) {
36
- return settings.env.LETTA_API_KEY;
37
- }
38
- } catch {
39
- // Settings file doesn't exist or is invalid
40
- }
41
-
42
- throw new Error(
43
- "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
44
- );
45
- }
46
-
47
- const MEMORY_FS_STATE_FILE = ".sync-state.json";
48
-
49
- // Unified sync state format (matches main memoryFilesystem.ts)
50
- type SyncState = {
51
- blockHashes: Record<string, string>;
52
- fileHashes: Record<string, string>;
53
- blockIds: Record<string, string>;
54
- lastSync: string | null;
55
- };
56
-
57
- function hashContent(content: string): string {
58
- return createHash("sha256").update(content).digest("hex");
59
- }
60
-
61
- // parseFrontmatter/hashFileBody provided by shared helper
62
-
63
- function getMemoryRoot(agentId: string): string {
64
- return join(homedir(), ".letta", "agents", agentId, "memory");
65
- }
66
-
67
- function loadSyncState(agentId: string): SyncState {
68
- const statePath = join(getMemoryRoot(agentId), MEMORY_FS_STATE_FILE);
69
- if (!existsSync(statePath)) {
70
- return {
71
- blockHashes: {},
72
- fileHashes: {},
73
- blockIds: {},
74
- lastSync: null,
75
- };
76
- }
77
-
78
- try {
79
- const raw = readFileSync(statePath, "utf-8");
80
- const parsed = JSON.parse(raw);
81
- return {
82
- blockHashes: parsed.blockHashes || {},
83
- fileHashes: parsed.fileHashes || {},
84
- blockIds: parsed.blockIds || {},
85
- lastSync: parsed.lastSync || null,
86
- };
87
- } catch {
88
- return {
89
- blockHashes: {},
90
- fileHashes: {},
91
- blockIds: {},
92
- lastSync: null,
93
- };
94
- }
95
- }
96
-
97
- async function scanMdFiles(
98
- dir: string,
99
- baseDir = dir,
100
- excludeDirs: string[] = [],
101
- ): Promise<string[]> {
102
- if (!existsSync(dir)) return [];
103
- const entries = await readdir(dir, { withFileTypes: true });
104
- const results: string[] = [];
105
- for (const entry of entries) {
106
- const fullPath = join(dir, entry.name);
107
- if (entry.isDirectory()) {
108
- if (excludeDirs.includes(entry.name)) continue;
109
- results.push(...(await scanMdFiles(fullPath, baseDir, excludeDirs)));
110
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
111
- results.push(relative(baseDir, fullPath));
112
- }
113
- }
114
- return results;
115
- }
116
-
117
- function labelFromPath(relativePath: string): string {
118
- return relativePath.replace(/\\/g, "/").replace(/\.md$/, "");
119
- }
120
-
121
- async function readMemoryFiles(
122
- dir: string,
123
- excludeDirs: string[] = [],
124
- ): Promise<Map<string, { content: string }>> {
125
- const files = await scanMdFiles(dir, dir, excludeDirs);
126
- const entries = new Map<string, { content: string }>();
127
- for (const rel of files) {
128
- const label = labelFromPath(rel);
129
- const content = await readFile(join(dir, rel), "utf-8");
130
- entries.set(label, { content });
131
- }
132
- return entries;
133
- }
134
-
135
- // Only memory_filesystem is managed by memfs itself
136
- const MEMFS_MANAGED_LABELS = new Set(["memory_filesystem"]);
137
-
138
- interface StatusResult {
139
- conflicts: Array<{ label: string }>;
140
- pendingFromFile: string[];
141
- pendingFromBlock: string[];
142
- newFiles: string[];
143
- newBlocks: string[];
144
- locationMismatches: string[];
145
- isClean: boolean;
146
- lastSync: string | null;
147
- }
148
-
149
- async function checkStatus(agentId: string): Promise<StatusResult> {
150
- const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
151
- const client = new Letta({ apiKey: getApiKey(), baseUrl });
152
-
153
- const root = getMemoryRoot(agentId);
154
- const systemDir = join(root, "system");
155
- const detachedDir = root;
156
-
157
- // Ensure directories exist
158
- for (const dir of [root, systemDir]) {
159
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
160
- }
161
-
162
- // Read files from both locations
163
- const systemFiles = await readMemoryFiles(systemDir);
164
- const detachedFiles = await readMemoryFiles(detachedDir, ["system", "user"]);
165
-
166
- // Fetch attached blocks
167
- const blocksResponse = await client.agents.blocks.list(agentId, {
168
- limit: 1000,
169
- });
170
- const attachedBlocks = Array.isArray(blocksResponse)
171
- ? blocksResponse
172
- : ((blocksResponse as { items?: unknown[] }).items as Array<{
173
- id?: string;
174
- label?: string;
175
- value?: string;
176
- read_only?: boolean;
177
- }>) || [];
178
-
179
- const systemBlockMap = new Map<
180
- string,
181
- { value: string; id: string; read_only?: boolean }
182
- >();
183
- for (const block of attachedBlocks) {
184
- if (block.label && block.id) {
185
- systemBlockMap.set(block.label, {
186
- value: block.value || "",
187
- id: block.id,
188
- read_only: block.read_only,
189
- });
190
- }
191
- }
192
-
193
- // Fetch detached blocks via owner tag
194
- const ownedBlocksResponse = await client.blocks.list({
195
- tags: [`owner:${agentId}`],
196
- limit: 1000,
197
- });
198
- const ownedBlocks = Array.isArray(ownedBlocksResponse)
199
- ? ownedBlocksResponse
200
- : ((ownedBlocksResponse as { items?: unknown[] }).items as Array<{
201
- id?: string;
202
- label?: string;
203
- value?: string;
204
- read_only?: boolean;
205
- }>) || [];
206
-
207
- const attachedIds = new Set(attachedBlocks.map((b) => b.id));
208
- const detachedBlockMap = new Map<
209
- string,
210
- { value: string; id: string; read_only?: boolean }
211
- >();
212
- for (const block of ownedBlocks) {
213
- if (block.label && block.id && !attachedIds.has(block.id)) {
214
- if (!systemBlockMap.has(block.label)) {
215
- detachedBlockMap.set(block.label, {
216
- value: block.value || "",
217
- id: block.id,
218
- read_only: block.read_only,
219
- });
220
- }
221
- }
222
- }
223
-
224
- const lastState = loadSyncState(agentId);
225
-
226
- const conflicts: Array<{ label: string }> = [];
227
- const pendingFromFile: string[] = [];
228
- const pendingFromBlock: string[] = [];
229
- const newFiles: string[] = [];
230
- const newBlocks: string[] = [];
231
- const locationMismatches: string[] = [];
232
-
233
- // Collect all labels
234
- const allLabels = new Set<string>([
235
- ...systemFiles.keys(),
236
- ...detachedFiles.keys(),
237
- ...systemBlockMap.keys(),
238
- ...detachedBlockMap.keys(),
239
- ...Object.keys(lastState.blockHashes),
240
- ...Object.keys(lastState.fileHashes),
241
- ]);
242
-
243
- for (const label of [...allLabels].sort()) {
244
- if (MEMFS_MANAGED_LABELS.has(label)) continue;
245
-
246
- const systemFile = systemFiles.get(label);
247
- const detachedFile = detachedFiles.get(label);
248
- const attachedBlock = systemBlockMap.get(label);
249
- const detachedBlock = detachedBlockMap.get(label);
250
-
251
- const fileEntry = systemFile || detachedFile;
252
- const fileInSystem = !!systemFile;
253
- const blockEntry = attachedBlock || detachedBlock;
254
- const isAttached = !!attachedBlock;
255
- const effectiveReadOnly =
256
- !!blockEntry?.read_only || READ_ONLY_LABELS.has(label);
257
-
258
- // Check for location mismatch
259
- if (fileEntry && blockEntry) {
260
- const locationMismatch =
261
- (fileInSystem && !isAttached) || (!fileInSystem && isAttached);
262
- if (locationMismatch) {
263
- locationMismatches.push(label);
264
- }
265
- }
266
-
267
- // Compute hashes
268
- // Full file hash for "file changed" check (matches what's stored in fileHashes)
269
- const fileHash = fileEntry ? hashContent(fileEntry.content) : null;
270
- // Body hash for "content matches" check (compares to block value)
271
- const fileBodyHash = fileEntry ? hashFileBody(fileEntry.content) : null;
272
- const blockHash = blockEntry ? hashContent(blockEntry.value) : null;
273
-
274
- const lastFileHash = lastState.fileHashes[label] ?? null;
275
- const lastBlockHash = lastState.blockHashes[label] ?? null;
276
-
277
- const fileChanged = fileHash !== lastFileHash;
278
- const blockChanged = blockHash !== lastBlockHash;
279
-
280
- // Classify
281
- if (fileEntry && !blockEntry) {
282
- if (READ_ONLY_LABELS.has(label)) continue; // API authoritative, file-only will be deleted on sync
283
- if (lastBlockHash && !fileChanged) continue; // Block deleted, file unchanged
284
- newFiles.push(label);
285
- continue;
286
- }
287
-
288
- if (!fileEntry && blockEntry) {
289
- if (effectiveReadOnly) {
290
- pendingFromFile.push(label);
291
- continue;
292
- }
293
- if (lastFileHash && !blockChanged) continue; // File deleted, block unchanged
294
- newBlocks.push(label);
295
- continue;
296
- }
297
-
298
- if (!fileEntry || !blockEntry) continue;
299
-
300
- // Both exist - read_only blocks are API-authoritative
301
- if (effectiveReadOnly) {
302
- if (blockChanged) pendingFromBlock.push(label);
303
- continue;
304
- }
305
-
306
- // Both exist - check if content matches (body vs block value)
307
- if (fileBodyHash === blockHash) {
308
- if (fileChanged) {
309
- // Frontmatter-only change; content matches
310
- pendingFromFile.push(label);
311
- }
312
- continue;
313
- }
314
-
315
- // "FS wins all" policy: if file changed, treat as pendingFromFile
316
- if (fileChanged) {
317
- pendingFromFile.push(label);
318
- continue;
319
- }
320
-
321
- if (blockChanged) {
322
- pendingFromBlock.push(label);
323
- }
324
- }
325
-
326
- const isClean =
327
- conflicts.length === 0 &&
328
- pendingFromFile.length === 0 &&
329
- pendingFromBlock.length === 0 &&
330
- newFiles.length === 0 &&
331
- newBlocks.length === 0 &&
332
- locationMismatches.length === 0;
333
-
334
- return {
335
- conflicts,
336
- pendingFromFile,
337
- pendingFromBlock,
338
- newFiles,
339
- newBlocks,
340
- locationMismatches,
341
- isClean,
342
- lastSync: lastState.lastSync,
343
- };
344
- }
345
-
346
- // CLI Entry Point
347
- const isMainModule = import.meta.url === `file://${process.argv[1]}`;
348
- if (isMainModule) {
349
- const args = process.argv.slice(2);
350
-
351
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
352
- console.log(`
353
- Usage: npx tsx memfs-status.ts <agent-id>
354
-
355
- Shows the current memFS sync status (read-only).
356
- Analogous to 'git status'.
357
-
358
- Arguments:
359
- agent-id Agent ID to check (can use $LETTA_AGENT_ID)
360
-
361
- Output: JSON object with:
362
- - conflicts: blocks where both file and block changed
363
- - pendingFromFile: file changed, block didn't
364
- - pendingFromBlock: block changed, file didn't
365
- - newFiles: file exists without a block
366
- - newBlocks: block exists without a file
367
- - locationMismatches: file/block location doesn't match attachment
368
- - isClean: true if everything is in sync
369
- - lastSync: timestamp of last sync
370
- `);
371
- process.exit(0);
372
- }
373
-
374
- const agentId = args[0];
375
- if (!agentId) {
376
- console.error("Error: agent-id is required");
377
- process.exit(1);
378
- }
379
-
380
- checkStatus(agentId)
381
- .then((status) => {
382
- console.log(JSON.stringify(status, null, 2));
383
- })
384
- .catch((error) => {
385
- console.error(
386
- "Error checking memFS status:",
387
- error instanceof Error ? error.message : String(error),
388
- );
389
- process.exit(1);
390
- });
391
- }