@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,200 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
-
3
- /**
4
- * Search Messages - Search past conversations with vector/FTS search
5
- *
6
- * This script is standalone and can be run outside the CLI process.
7
- * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
8
- * It reads agent ID from LETTA_AGENT_ID env var or --agent-id arg.
9
- *
10
- * Usage:
11
- * npx tsx search-messages.ts --query <text> [options]
12
- *
13
- * Options:
14
- * --query <text> Search query (required)
15
- * --mode <mode> Search mode: vector, fts, hybrid (default: hybrid)
16
- * --start-date <date> Filter messages after this date (ISO format)
17
- * --end-date <date> Filter messages before this date (ISO format)
18
- * --limit <n> Max results (default: 10)
19
- * --all-agents Search all agents, not just current agent
20
- * --agent-id <id> Explicit agent ID (overrides LETTA_AGENT_ID env var)
21
- *
22
- * Output:
23
- * Raw API response with search results
24
- */
25
-
26
- import { readFileSync } from "node:fs";
27
- import { createRequire } from "node:module";
28
- import { homedir } from "node:os";
29
- import { join } from "node:path";
30
-
31
- // Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
32
- // (ES module imports don't respect NODE_PATH, but require does)
33
- const require = createRequire(import.meta.url);
34
- const Letta = require("@letta-ai/letta-client")
35
- .default as typeof import("@letta-ai/letta-client").default;
36
- type LettaClient = InstanceType<typeof Letta>;
37
-
38
- interface SearchMessagesOptions {
39
- query: string;
40
- mode?: "vector" | "fts" | "hybrid";
41
- startDate?: string;
42
- endDate?: string;
43
- limit?: number;
44
- allAgents?: boolean;
45
- agentId?: string;
46
- }
47
-
48
- /**
49
- * Get API key from env var or settings file
50
- */
51
- function getApiKey(): string {
52
- // First check env var (set by CLI's getShellEnv)
53
- if (process.env.LETTA_API_KEY) {
54
- return process.env.LETTA_API_KEY;
55
- }
56
-
57
- // Fall back to settings file
58
- const settingsPath = join(homedir(), ".letta", "settings.json");
59
- try {
60
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
61
- if (settings.env?.LETTA_API_KEY) {
62
- return settings.env.LETTA_API_KEY;
63
- }
64
- } catch {
65
- // Settings file doesn't exist or is invalid
66
- }
67
-
68
- throw new Error(
69
- "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
70
- );
71
- }
72
-
73
- /**
74
- * Get agent ID from CLI arg, env var, or throw
75
- */
76
- function getAgentId(cliArg?: string): string {
77
- // CLI arg takes precedence
78
- if (cliArg) return cliArg;
79
-
80
- // Then env var (set by CLI's getShellEnv)
81
- if (process.env.LETTA_AGENT_ID) {
82
- return process.env.LETTA_AGENT_ID;
83
- }
84
-
85
- throw new Error(
86
- "No agent ID provided. Use --agent-id or ensure LETTA_AGENT_ID env var is set.",
87
- );
88
- }
89
-
90
- /**
91
- * Create a Letta client with auth from env/settings
92
- */
93
- function createClient(): LettaClient {
94
- return new Letta({ apiKey: getApiKey() });
95
- }
96
-
97
- /**
98
- * Search messages in past conversations
99
- * @param client - Letta client instance
100
- * @param options - Search options
101
- * @returns Array of search results with scores
102
- */
103
- export async function searchMessages(
104
- client: LettaClient,
105
- options: SearchMessagesOptions,
106
- ): Promise<Awaited<ReturnType<typeof client.messages.search>>> {
107
- // Default to current agent unless --all-agents is specified
108
- let agentId: string | undefined;
109
- if (!options.allAgents) {
110
- agentId = getAgentId(options.agentId);
111
- }
112
-
113
- return await client.messages.search({
114
- query: options.query,
115
- agent_id: agentId,
116
- search_mode: options.mode ?? "hybrid",
117
- start_date: options.startDate,
118
- end_date: options.endDate,
119
- limit: options.limit ?? 10,
120
- });
121
- }
122
-
123
- function parseArgs(args: string[]): SearchMessagesOptions {
124
- const queryIndex = args.indexOf("--query");
125
- if (queryIndex === -1 || queryIndex + 1 >= args.length) {
126
- throw new Error("Missing required argument: --query <text>");
127
- }
128
-
129
- const options: SearchMessagesOptions = {
130
- query: args[queryIndex + 1] as string,
131
- };
132
-
133
- const modeIndex = args.indexOf("--mode");
134
- if (modeIndex !== -1 && modeIndex + 1 < args.length) {
135
- const mode = args[modeIndex + 1] as string;
136
- if (mode === "vector" || mode === "fts" || mode === "hybrid") {
137
- options.mode = mode;
138
- }
139
- }
140
-
141
- const startDateIndex = args.indexOf("--start-date");
142
- if (startDateIndex !== -1 && startDateIndex + 1 < args.length) {
143
- options.startDate = args[startDateIndex + 1];
144
- }
145
-
146
- const endDateIndex = args.indexOf("--end-date");
147
- if (endDateIndex !== -1 && endDateIndex + 1 < args.length) {
148
- options.endDate = args[endDateIndex + 1];
149
- }
150
-
151
- const limitIndex = args.indexOf("--limit");
152
- if (limitIndex !== -1 && limitIndex + 1 < args.length) {
153
- const limit = Number.parseInt(args[limitIndex + 1] as string, 10);
154
- if (!Number.isNaN(limit)) {
155
- options.limit = limit;
156
- }
157
- }
158
-
159
- if (args.includes("--all-agents")) {
160
- options.allAgents = true;
161
- }
162
-
163
- const agentIdIndex = args.indexOf("--agent-id");
164
- if (agentIdIndex !== -1 && agentIdIndex + 1 < args.length) {
165
- options.agentId = args[agentIdIndex + 1];
166
- }
167
-
168
- return options;
169
- }
170
-
171
- // CLI entry point - check if this file is being run directly
172
- const isMainModule = import.meta.url === `file://${process.argv[1]}`;
173
- if (isMainModule) {
174
- (async () => {
175
- try {
176
- const options = parseArgs(process.argv.slice(2));
177
- const client = createClient();
178
- const result = await searchMessages(client, options);
179
- console.log(JSON.stringify(result, null, 2));
180
- } catch (error) {
181
- console.error(
182
- "Error:",
183
- error instanceof Error ? error.message : String(error),
184
- );
185
- console.error(`
186
- Usage: npx tsx search-messages.ts --query <text> [options]
187
-
188
- Options:
189
- --query <text> Search query (required)
190
- --mode <mode> Search mode: vector, fts, hybrid (default: hybrid)
191
- --start-date <date> Filter messages after this date (ISO format)
192
- --end-date <date> Filter messages before this date (ISO format)
193
- --limit <n> Max results (default: 10)
194
- --all-agents Search all agents, not just current agent
195
- --agent-id <id> Explicit agent ID (overrides LETTA_AGENT_ID env var)
196
- `);
197
- process.exit(1);
198
- }
199
- })();
200
- }
@@ -1,47 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { READ_ONLY_BLOCK_LABELS } from "../../../../../agent/memoryConstants";
3
-
4
- /**
5
- * Read-only block labels. These are API-authoritative.
6
- */
7
- export const READ_ONLY_LABELS = new Set(
8
- READ_ONLY_BLOCK_LABELS as readonly string[],
9
- );
10
-
11
- /**
12
- * Parse MDX-style frontmatter from content.
13
- * This is a copy of parseMdxFrontmatter from src/agent/memory.ts.
14
- * The test ensures this stays in sync with the original.
15
- */
16
- export function parseFrontmatter(content: string): {
17
- frontmatter: Record<string, string>;
18
- body: string;
19
- } {
20
- const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
21
- const match = content.match(frontmatterRegex);
22
-
23
- if (!match || !match[1] || !match[2]) {
24
- return { frontmatter: {}, body: content };
25
- }
26
-
27
- const frontmatterText = match[1];
28
- const body = match[2];
29
- const frontmatter: Record<string, string> = {};
30
-
31
- // Parse YAML-like frontmatter (simple key: value pairs)
32
- for (const line of frontmatterText.split("\n")) {
33
- const colonIndex = line.indexOf(":");
34
- if (colonIndex > 0) {
35
- const key = line.slice(0, colonIndex).trim();
36
- const value = line.slice(colonIndex + 1).trim();
37
- frontmatter[key] = value;
38
- }
39
- }
40
-
41
- return { frontmatter, body: body.trim() };
42
- }
43
-
44
- export function hashFileBody(content: string): string {
45
- const { body } = parseFrontmatter(content);
46
- return createHash("sha256").update(body).digest("hex");
47
- }
@@ -1,430 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * Memory Filesystem Diff
4
- *
5
- * Shows the full content of conflicting blocks and files.
6
- * Writes a formatted markdown diff to a file for review.
7
- * Analogous to `git diff`.
8
- *
9
- * Usage:
10
- * npx tsx memfs-diff.ts <agent-id>
11
- *
12
- * Output: Path to the diff file (or "No conflicts" message)
13
- */
14
-
15
- import { createHash, randomUUID } from "node:crypto";
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } 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, normalize, 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 Conflict {
139
- label: string;
140
- fileContent: string;
141
- blockContent: string;
142
- }
143
-
144
- interface MetadataChange {
145
- label: string;
146
- fileContent: string;
147
- blockContent: string;
148
- }
149
-
150
- /**
151
- * Get the overflow directory following the same pattern as tool output overflow.
152
- * Pattern: ~/.letta/projects/<project-path>/agent-tools/
153
- */
154
- function getOverflowDirectory(): string {
155
- const cwd = process.cwd();
156
- const normalizedPath = normalize(cwd);
157
- const sanitizedPath = normalizedPath
158
- .replace(/^[/\\]/, "")
159
- .replace(/[/\\:]/g, "_")
160
- .replace(/\s+/g, "_");
161
-
162
- return join(homedir(), ".letta", "projects", sanitizedPath, "agent-tools");
163
- }
164
-
165
- async function findConflicts(agentId: string): Promise<{
166
- conflicts: Conflict[];
167
- metadataOnly: MetadataChange[];
168
- }> {
169
- const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
170
- const client = new Letta({ apiKey: getApiKey(), baseUrl });
171
-
172
- const root = getMemoryRoot(agentId);
173
- const systemDir = join(root, "system");
174
- const detachedDir = root;
175
-
176
- for (const dir of [root, systemDir]) {
177
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
178
- }
179
-
180
- // Read files from both locations
181
- const systemFiles = await readMemoryFiles(systemDir);
182
- const detachedFiles = await readMemoryFiles(detachedDir, ["system", "user"]);
183
-
184
- // Fetch attached blocks
185
- const blocksResponse = await client.agents.blocks.list(agentId, {
186
- limit: 1000,
187
- });
188
- const attachedBlocks = Array.isArray(blocksResponse)
189
- ? blocksResponse
190
- : ((blocksResponse as { items?: unknown[] }).items as Array<{
191
- id?: string;
192
- label?: string;
193
- value?: string;
194
- read_only?: boolean;
195
- }>) || [];
196
-
197
- const systemBlockMap = new Map<
198
- string,
199
- { value: string; id: string; read_only?: boolean }
200
- >();
201
- for (const block of attachedBlocks) {
202
- if (block.label && block.id) {
203
- systemBlockMap.set(block.label, {
204
- value: block.value || "",
205
- id: block.id,
206
- read_only: block.read_only,
207
- });
208
- }
209
- }
210
-
211
- // Fetch detached blocks via owner tag
212
- const ownedBlocksResponse = await client.blocks.list({
213
- tags: [`owner:${agentId}`],
214
- limit: 1000,
215
- });
216
- const ownedBlocks = Array.isArray(ownedBlocksResponse)
217
- ? ownedBlocksResponse
218
- : ((ownedBlocksResponse as { items?: unknown[] }).items as Array<{
219
- id?: string;
220
- label?: string;
221
- value?: string;
222
- read_only?: boolean;
223
- }>) || [];
224
-
225
- const attachedIds = new Set(attachedBlocks.map((b) => b.id));
226
- const detachedBlockMap = new Map<
227
- string,
228
- { value: string; id: string; read_only?: boolean }
229
- >();
230
- for (const block of ownedBlocks) {
231
- if (block.label && block.id && !attachedIds.has(block.id)) {
232
- if (!systemBlockMap.has(block.label)) {
233
- detachedBlockMap.set(block.label, {
234
- value: block.value || "",
235
- id: block.id,
236
- read_only: block.read_only,
237
- });
238
- }
239
- }
240
- }
241
-
242
- const lastState = loadSyncState(agentId);
243
- const conflicts: Conflict[] = [];
244
- const metadataOnly: MetadataChange[] = [];
245
-
246
- // Collect all labels
247
- const allLabels = new Set<string>([
248
- ...systemFiles.keys(),
249
- ...detachedFiles.keys(),
250
- ...systemBlockMap.keys(),
251
- ...detachedBlockMap.keys(),
252
- ...Object.keys(lastState.blockHashes),
253
- ...Object.keys(lastState.fileHashes),
254
- ]);
255
-
256
- for (const label of [...allLabels].sort()) {
257
- if (MEMFS_MANAGED_LABELS.has(label)) continue;
258
-
259
- const systemFile = systemFiles.get(label);
260
- const detachedFile = detachedFiles.get(label);
261
- const attachedBlock = systemBlockMap.get(label);
262
- const detachedBlock = detachedBlockMap.get(label);
263
-
264
- const fileEntry = systemFile || detachedFile;
265
- const blockEntry = attachedBlock || detachedBlock;
266
-
267
- if (!fileEntry || !blockEntry) continue;
268
-
269
- // read_only blocks are API-authoritative; no conflicts possible
270
- const effectiveReadOnly =
271
- !!blockEntry.read_only || READ_ONLY_LABELS.has(label);
272
- if (effectiveReadOnly) continue;
273
-
274
- // Full file hash for "file changed" check
275
- const fileHash = hashContent(fileEntry.content);
276
- // Body hash for "content matches" check
277
- const fileBodyHash = hashFileBody(fileEntry.content);
278
- const blockHash = hashContent(blockEntry.value);
279
-
280
- const lastFileHash = lastState.fileHashes[label] ?? null;
281
- const lastBlockHash = lastState.blockHashes[label] ?? null;
282
- const fileChanged = fileHash !== lastFileHash;
283
- const blockChanged = blockHash !== lastBlockHash;
284
-
285
- // Content matches - check for frontmatter-only changes
286
- if (fileBodyHash === blockHash) {
287
- if (fileChanged) {
288
- metadataOnly.push({
289
- label,
290
- fileContent: fileEntry.content,
291
- blockContent: blockEntry.value,
292
- });
293
- }
294
- continue;
295
- }
296
-
297
- // Conflict only if both changed
298
- if (fileChanged && blockChanged) {
299
- conflicts.push({
300
- label,
301
- fileContent: fileEntry.content,
302
- blockContent: blockEntry.value,
303
- });
304
- }
305
- }
306
-
307
- return { conflicts, metadataOnly };
308
- }
309
-
310
- function formatDiffFile(
311
- conflicts: Conflict[],
312
- metadataOnly: MetadataChange[],
313
- agentId: string,
314
- ): string {
315
- const lines: string[] = [
316
- `# Memory Filesystem Diff`,
317
- ``,
318
- `Agent: ${agentId}`,
319
- `Generated: ${new Date().toISOString()}`,
320
- `Conflicts: ${conflicts.length}`,
321
- `Metadata-only changes: ${metadataOnly.length}`,
322
- ``,
323
- `---`,
324
- ``,
325
- ];
326
-
327
- for (const conflict of conflicts) {
328
- lines.push(`## Conflict: ${conflict.label}`);
329
- lines.push(``);
330
- lines.push(`### File Version`);
331
- lines.push(`\`\`\``);
332
- lines.push(conflict.fileContent);
333
- lines.push(`\`\`\``);
334
- lines.push(``);
335
- lines.push(`### Block Version`);
336
- lines.push(`\`\`\``);
337
- lines.push(conflict.blockContent);
338
- lines.push(`\`\`\``);
339
- lines.push(``);
340
- lines.push(`---`);
341
- lines.push(``);
342
- }
343
-
344
- if (metadataOnly.length > 0) {
345
- lines.push(`## Metadata-only Changes`);
346
- lines.push(``);
347
- lines.push(
348
- `Frontmatter changed while body content stayed the same (file wins).`,
349
- );
350
- lines.push(``);
351
-
352
- for (const change of metadataOnly) {
353
- lines.push(`### ${change.label}`);
354
- lines.push(``);
355
- lines.push(`#### File Version (with frontmatter)`);
356
- lines.push(`\`\`\``);
357
- lines.push(change.fileContent);
358
- lines.push(`\`\`\``);
359
- lines.push(``);
360
- lines.push(`#### Block Version (body only)`);
361
- lines.push(`\`\`\``);
362
- lines.push(change.blockContent);
363
- lines.push(`\`\`\``);
364
- lines.push(``);
365
- lines.push(`---`);
366
- lines.push(``);
367
- }
368
- }
369
-
370
- return lines.join("\n");
371
- }
372
-
373
- // CLI Entry Point
374
- const isMainModule = import.meta.url === `file://${process.argv[1]}`;
375
- if (isMainModule) {
376
- const args = process.argv.slice(2);
377
-
378
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
379
- console.log(`
380
- Usage: npx tsx memfs-diff.ts <agent-id>
381
-
382
- Shows the full content of conflicting memory blocks and files.
383
- Writes a formatted diff to a file for review.
384
- Analogous to 'git diff'.
385
-
386
- Arguments:
387
- agent-id Agent ID to check (can use $LETTA_AGENT_ID)
388
-
389
- Output: Path to the diff file, or a message if no conflicts exist.
390
- `);
391
- process.exit(0);
392
- }
393
-
394
- const agentId = args[0];
395
- if (!agentId) {
396
- console.error("Error: agent-id is required");
397
- process.exit(1);
398
- }
399
-
400
- findConflicts(agentId)
401
- .then(({ conflicts, metadataOnly }) => {
402
- if (conflicts.length === 0 && metadataOnly.length === 0) {
403
- console.log("No conflicts found. Memory filesystem is clean.");
404
- return;
405
- }
406
-
407
- const diffContent = formatDiffFile(conflicts, metadataOnly, agentId);
408
-
409
- // Write to overflow directory (same pattern as tool output overflow)
410
- const overflowDir = getOverflowDirectory();
411
- if (!existsSync(overflowDir)) {
412
- mkdirSync(overflowDir, { recursive: true });
413
- }
414
-
415
- const filename = `memfs-diff-${randomUUID()}.md`;
416
- const diffPath = join(overflowDir, filename);
417
- writeFileSync(diffPath, diffContent, "utf-8");
418
-
419
- console.log(
420
- `Diff (${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}, ${metadataOnly.length} metadata-only change${metadataOnly.length === 1 ? "" : "s"}) written to: ${diffPath}`,
421
- );
422
- })
423
- .catch((error) => {
424
- console.error(
425
- "Error generating memFS diff:",
426
- error instanceof Error ? error.message : String(error),
427
- );
428
- process.exit(1);
429
- });
430
- }