@letta-ai/letta-code 0.11.1 → 0.11.2-next.2
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/letta.js +4313 -5374
- package/package.json +2 -2
- package/skills/finding-agents/SKILL.md +115 -0
- package/skills/finding-agents/scripts/find-agents.ts +177 -0
- package/skills/initializing-memory/SKILL.md +12 -0
- package/skills/migrating-memory/SKILL.md +132 -0
- package/skills/migrating-memory/scripts/attach-block.ts +161 -0
- package/skills/migrating-memory/scripts/copy-block.ts +173 -0
- package/skills/migrating-memory/scripts/get-agent-blocks.ts +103 -0
- package/skills/searching-messages/SKILL.md +127 -0
- package/skills/searching-messages/scripts/get-messages.ts +230 -0
- package/skills/searching-messages/scripts/search-messages.ts +200 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Copy Block - Copies a memory block to create a new independent block for the current agent
|
|
4
|
+
*
|
|
5
|
+
* This script is standalone and can be run outside the CLI process.
|
|
6
|
+
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
|
7
|
+
* It reads agent ID from LETTA_AGENT_ID env var or --agent-id arg.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --label Override the block label (required if you already have a block with that label)
|
|
14
|
+
* --agent-id Target agent ID (overrides LETTA_AGENT_ID env var)
|
|
15
|
+
*
|
|
16
|
+
* This creates a new block with the same content as the source block,
|
|
17
|
+
* then attaches it to the current agent. Changes to the new block
|
|
18
|
+
* won't affect the original.
|
|
19
|
+
*
|
|
20
|
+
* Output:
|
|
21
|
+
* Raw API response from each step (retrieve, create, attach)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
|
30
|
+
// (ES module imports don't respect NODE_PATH, but require does)
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const Letta = require("@letta-ai/letta-client")
|
|
33
|
+
.default as typeof import("@letta-ai/letta-client").default;
|
|
34
|
+
type LettaClient = InstanceType<typeof Letta>;
|
|
35
|
+
|
|
36
|
+
interface CopyBlockResult {
|
|
37
|
+
sourceBlock: Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>;
|
|
38
|
+
newBlock: Awaited<ReturnType<LettaClient["blocks"]["create"]>>;
|
|
39
|
+
attachResult: Awaited<ReturnType<LettaClient["agents"]["blocks"]["attach"]>>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get API key from env var or settings file
|
|
44
|
+
*/
|
|
45
|
+
function getApiKey(): string {
|
|
46
|
+
if (process.env.LETTA_API_KEY) {
|
|
47
|
+
return process.env.LETTA_API_KEY;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const settingsPath = join(homedir(), ".letta", "settings.json");
|
|
51
|
+
try {
|
|
52
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
53
|
+
if (settings.env?.LETTA_API_KEY) {
|
|
54
|
+
return settings.env.LETTA_API_KEY;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Settings file doesn't exist or is invalid
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(
|
|
61
|
+
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get agent ID from CLI arg, env var, or throw
|
|
67
|
+
*/
|
|
68
|
+
function getAgentId(cliArg?: string): string {
|
|
69
|
+
if (cliArg) return cliArg;
|
|
70
|
+
if (process.env.LETTA_AGENT_ID) {
|
|
71
|
+
return process.env.LETTA_AGENT_ID;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(
|
|
74
|
+
"No agent ID provided. Use --agent-id or ensure LETTA_AGENT_ID env var is set.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a Letta client with auth from env/settings
|
|
80
|
+
*/
|
|
81
|
+
function createClient(): LettaClient {
|
|
82
|
+
return new Letta({ apiKey: getApiKey() });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Copy a block's content to a new block and attach to the current agent
|
|
87
|
+
* @param client - Letta client instance
|
|
88
|
+
* @param blockId - The source block ID to copy from
|
|
89
|
+
* @param options - Optional settings: labelOverride, targetAgentId
|
|
90
|
+
* @returns Object containing source block, new block, and attach result
|
|
91
|
+
*/
|
|
92
|
+
export async function copyBlock(
|
|
93
|
+
client: LettaClient,
|
|
94
|
+
blockId: string,
|
|
95
|
+
options?: { labelOverride?: string; targetAgentId?: string },
|
|
96
|
+
): Promise<CopyBlockResult> {
|
|
97
|
+
// Get current agent ID (the agent calling this script) or use provided ID
|
|
98
|
+
const currentAgentId = getAgentId(options?.targetAgentId);
|
|
99
|
+
|
|
100
|
+
// 1. Get source block details
|
|
101
|
+
const sourceBlock = await client.blocks.retrieve(blockId);
|
|
102
|
+
|
|
103
|
+
// 2. Create new block with same content (optionally override label)
|
|
104
|
+
const newBlock = await client.blocks.create({
|
|
105
|
+
label: options?.labelOverride || sourceBlock.label || "migrated-block",
|
|
106
|
+
value: sourceBlock.value,
|
|
107
|
+
description: sourceBlock.description || undefined,
|
|
108
|
+
limit: sourceBlock.limit,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 3. Attach new block to current agent
|
|
112
|
+
const attachResult = await client.agents.blocks.attach(newBlock.id, {
|
|
113
|
+
agent_id: currentAgentId,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return { sourceBlock, newBlock, attachResult };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseArgs(args: string[]): {
|
|
120
|
+
blockId: string;
|
|
121
|
+
label?: string;
|
|
122
|
+
agentId?: string;
|
|
123
|
+
} {
|
|
124
|
+
const blockIdIndex = args.indexOf("--block-id");
|
|
125
|
+
const labelIndex = args.indexOf("--label");
|
|
126
|
+
const agentIdIndex = args.indexOf("--agent-id");
|
|
127
|
+
|
|
128
|
+
if (blockIdIndex === -1 || blockIdIndex + 1 >= args.length) {
|
|
129
|
+
throw new Error("Missing required argument: --block-id <block-id>");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
blockId: args[blockIdIndex + 1] as string,
|
|
134
|
+
label:
|
|
135
|
+
labelIndex !== -1 && labelIndex + 1 < args.length
|
|
136
|
+
? (args[labelIndex + 1] as string)
|
|
137
|
+
: undefined,
|
|
138
|
+
agentId:
|
|
139
|
+
agentIdIndex !== -1 && agentIdIndex + 1 < args.length
|
|
140
|
+
? (args[agentIdIndex + 1] as string)
|
|
141
|
+
: undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// CLI entry point - check if this file is being run directly
|
|
146
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
147
|
+
if (isMainModule) {
|
|
148
|
+
(async () => {
|
|
149
|
+
try {
|
|
150
|
+
const { blockId, label, agentId } = parseArgs(process.argv.slice(2));
|
|
151
|
+
const client = createClient();
|
|
152
|
+
const result = await copyBlock(client, blockId, {
|
|
153
|
+
labelOverride: label,
|
|
154
|
+
targetAgentId: agentId,
|
|
155
|
+
});
|
|
156
|
+
console.log(JSON.stringify(result, null, 2));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(
|
|
159
|
+
"Error:",
|
|
160
|
+
error instanceof Error ? error.message : String(error),
|
|
161
|
+
);
|
|
162
|
+
if (
|
|
163
|
+
error instanceof Error &&
|
|
164
|
+
error.message.includes("Missing required argument")
|
|
165
|
+
) {
|
|
166
|
+
console.error(
|
|
167
|
+
"\nUsage: npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>]",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Get Agent Blocks - Retrieves memory blocks from a specific agent
|
|
4
|
+
*
|
|
5
|
+
* This script is standalone and can be run outside the CLI process.
|
|
6
|
+
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx get-agent-blocks.ts --agent-id <agent-id>
|
|
10
|
+
*
|
|
11
|
+
* Output:
|
|
12
|
+
* Raw API response from GET /v1/agents/{id}/core-memory/blocks
|
|
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
|
+
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
|
21
|
+
// (ES module imports don't respect NODE_PATH, but require does)
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const Letta = require("@letta-ai/letta-client")
|
|
24
|
+
.default as typeof import("@letta-ai/letta-client").default;
|
|
25
|
+
type LettaClient = InstanceType<typeof Letta>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get API key from env var or settings file
|
|
29
|
+
*/
|
|
30
|
+
function getApiKey(): string {
|
|
31
|
+
if (process.env.LETTA_API_KEY) {
|
|
32
|
+
return process.env.LETTA_API_KEY;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const settingsPath = join(homedir(), ".letta", "settings.json");
|
|
36
|
+
try {
|
|
37
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
38
|
+
if (settings.env?.LETTA_API_KEY) {
|
|
39
|
+
return settings.env.LETTA_API_KEY;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Settings file doesn't exist or is invalid
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error(
|
|
46
|
+
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a Letta client with auth from env/settings
|
|
52
|
+
*/
|
|
53
|
+
function createClient(): LettaClient {
|
|
54
|
+
return new Letta({ apiKey: getApiKey() });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get memory blocks for a specific agent
|
|
59
|
+
* @param client - Letta client instance
|
|
60
|
+
* @param agentId - The agent ID to get blocks from
|
|
61
|
+
* @returns Array of block objects from the API
|
|
62
|
+
*/
|
|
63
|
+
export async function getAgentBlocks(
|
|
64
|
+
client: LettaClient,
|
|
65
|
+
agentId: string,
|
|
66
|
+
): Promise<Awaited<ReturnType<typeof client.agents.blocks.list>>> {
|
|
67
|
+
return await client.agents.blocks.list(agentId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseArgs(args: string[]): { agentId: string } {
|
|
71
|
+
const agentIdIndex = args.indexOf("--agent-id");
|
|
72
|
+
if (agentIdIndex === -1 || agentIdIndex + 1 >= args.length) {
|
|
73
|
+
throw new Error("Missing required argument: --agent-id <agent-id>");
|
|
74
|
+
}
|
|
75
|
+
return { agentId: args[agentIdIndex + 1] as string };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// CLI entry point - check if this file is being run directly
|
|
79
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
80
|
+
if (isMainModule) {
|
|
81
|
+
(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const { agentId } = parseArgs(process.argv.slice(2));
|
|
84
|
+
const client = createClient();
|
|
85
|
+
const result = await getAgentBlocks(client, agentId);
|
|
86
|
+
console.log(JSON.stringify(result, null, 2));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(
|
|
89
|
+
"Error:",
|
|
90
|
+
error instanceof Error ? error.message : String(error),
|
|
91
|
+
);
|
|
92
|
+
if (
|
|
93
|
+
error instanceof Error &&
|
|
94
|
+
error.message.includes("Missing required argument")
|
|
95
|
+
) {
|
|
96
|
+
console.error(
|
|
97
|
+
"\nUsage: npx tsx get-agent-blocks.ts --agent-id <agent-id>",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: searching-messages
|
|
3
|
+
description: Search past messages to recall context. Use when you need to remember previous discussions, find specific topics mentioned before, pull up context from earlier in the conversation history, or find which agent discussed a topic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Searching Messages
|
|
7
|
+
|
|
8
|
+
This skill helps you search through past conversations to recall context that may have fallen out of your context window.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- User asks "do you remember when we discussed X?"
|
|
13
|
+
- You need context from an earlier conversation
|
|
14
|
+
- User references something from the past that you don't have in context
|
|
15
|
+
- You want to verify what was said before about a topic
|
|
16
|
+
- You need to find which agent discussed a specific topic (use with `finding-agents` skill)
|
|
17
|
+
|
|
18
|
+
## Script Usage
|
|
19
|
+
|
|
20
|
+
The scripts are located in the `scripts/` subdirectory of this skill. Use the **Skill Directory** path shown above when loading this skill.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx tsx <SKILL_DIR>/scripts/search-messages.ts --query <text> [options]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Replace `<SKILL_DIR>` with the actual path from the `# Skill Directory:` line at the top of this loaded skill.
|
|
27
|
+
|
|
28
|
+
### Options
|
|
29
|
+
|
|
30
|
+
| Option | Description |
|
|
31
|
+
|--------|-------------|
|
|
32
|
+
| `--query <text>` | Search query (required) |
|
|
33
|
+
| `--mode <mode>` | Search mode: `vector`, `fts`, `hybrid` (default: hybrid) |
|
|
34
|
+
| `--start-date <date>` | Filter messages after this date (ISO format) |
|
|
35
|
+
| `--end-date <date>` | Filter messages before this date (ISO format) |
|
|
36
|
+
| `--limit <n>` | Max results (default: 10) |
|
|
37
|
+
| `--all-agents` | Search all agents, not just current agent |
|
|
38
|
+
| `--agent-id <id>` | Explicit agent ID (for manual testing) |
|
|
39
|
+
|
|
40
|
+
### Search Modes
|
|
41
|
+
|
|
42
|
+
- **hybrid** (default): Combines vector similarity + full-text search with RRF scoring
|
|
43
|
+
- **vector**: Semantic similarity search (good for conceptual matches)
|
|
44
|
+
- **fts**: Full-text search (good for exact phrases)
|
|
45
|
+
|
|
46
|
+
## Companion Script: get-messages.ts
|
|
47
|
+
|
|
48
|
+
Use this to expand around a found needle by message ID cursor:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx tsx <SKILL_DIR>/scripts/get-messages.ts [options]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Option | Description |
|
|
55
|
+
|--------|-------------|
|
|
56
|
+
| `--after <message-id>` | Get messages after this ID (cursor) |
|
|
57
|
+
| `--before <message-id>` | Get messages before this ID (cursor) |
|
|
58
|
+
| `--order <asc\|desc>` | Sort order (default: desc = newest first) |
|
|
59
|
+
| `--limit <n>` | Max results (default: 20) |
|
|
60
|
+
| `--agent-id <id>` | Explicit agent ID |
|
|
61
|
+
|
|
62
|
+
## Search Strategies
|
|
63
|
+
|
|
64
|
+
### Strategy 1: Needle + Expand (Recommended)
|
|
65
|
+
|
|
66
|
+
Use when you need full conversation context around a specific topic:
|
|
67
|
+
|
|
68
|
+
1. **Find the needle** - Search with keywords to discover relevant messages:
|
|
69
|
+
```bash
|
|
70
|
+
npx tsx <SKILL_DIR>/scripts/search-messages.ts --query "flicker inline approval" --limit 5
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. **Note the message_id** - Find the most relevant result and copy its `message_id`
|
|
74
|
+
|
|
75
|
+
3. **Expand before** - Get messages leading up to the needle:
|
|
76
|
+
```bash
|
|
77
|
+
npx tsx <SKILL_DIR>/scripts/get-messages.ts --before "message-xyz" --limit 10
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
4. **Expand after** - Get messages following the needle (use `--order asc` for chronological):
|
|
81
|
+
```bash
|
|
82
|
+
npx tsx <SKILL_DIR>/scripts/get-messages.ts --after "message-xyz" --order asc --limit 10
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Strategy 2: Date-Bounded Search
|
|
86
|
+
|
|
87
|
+
Use when you know approximately when something was discussed:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx tsx <SKILL_DIR>/scripts/search-messages.ts --query "topic" --start-date "2025-12-31T00:00:00Z" --end-date "2025-12-31T23:59:59Z" --limit 15
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Results are sorted by relevance within the date window.
|
|
94
|
+
|
|
95
|
+
### Strategy 3: Broad Discovery
|
|
96
|
+
|
|
97
|
+
Use when you're not sure what you're looking for:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx tsx <SKILL_DIR>/scripts/search-messages.ts --query "vague topic" --mode vector --limit 10
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Vector mode finds semantically similar messages even without exact keyword matches.
|
|
104
|
+
|
|
105
|
+
### Strategy 4: Find Which Agent Discussed Something
|
|
106
|
+
|
|
107
|
+
Use with `--all-agents` to search across all agents and identify which one discussed a topic:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx tsx <SKILL_DIR>/scripts/search-messages.ts --query "authentication refactor" --all-agents --limit 10
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Results include `agent_id` for each message. Use this to:
|
|
114
|
+
1. Find the agent that worked on a specific feature
|
|
115
|
+
2. Identify the right agent to ask follow-up questions
|
|
116
|
+
3. Cross-reference with the `finding-agents` skill to get agent details
|
|
117
|
+
|
|
118
|
+
**Tip:** Load both `searching-messages` and `finding-agents` skills together when you need to find and identify agents by topic.
|
|
119
|
+
|
|
120
|
+
## Search Output
|
|
121
|
+
|
|
122
|
+
Returns search results with:
|
|
123
|
+
- `message_id` - Use this for cursor-based expansion
|
|
124
|
+
- `message_type` - `user_message`, `assistant_message`, `reasoning_message`
|
|
125
|
+
- `content` or `reasoning` - The actual message text
|
|
126
|
+
- `created_at` - When the message was sent (ISO format)
|
|
127
|
+
- `agent_id` - Which agent the message belongs to
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get Messages - Retrieve messages from an agent in chronological order
|
|
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 get-messages.ts [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --start-date <date> Filter messages after this date (ISO format)
|
|
15
|
+
* --end-date <date> Filter messages before this date (ISO format)
|
|
16
|
+
* --limit <n> Max results (default: 20)
|
|
17
|
+
* --agent-id <id> Explicit agent ID (overrides LETTA_AGENT_ID env var)
|
|
18
|
+
* --after <message-id> Cursor: get messages after this ID
|
|
19
|
+
* --before <message-id> Cursor: get messages before this ID
|
|
20
|
+
* --order <asc|desc> Sort order (default: desc = newest first)
|
|
21
|
+
*
|
|
22
|
+
* Use this after search-messages.ts to expand around a found needle.
|
|
23
|
+
*
|
|
24
|
+
* Output:
|
|
25
|
+
* Messages in chronological order (filtered by date if specified)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { createRequire } from "node:module";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
|
|
33
|
+
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
|
34
|
+
// (ES module imports don't respect NODE_PATH, but require does)
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const Letta = require("@letta-ai/letta-client")
|
|
37
|
+
.default as typeof import("@letta-ai/letta-client").default;
|
|
38
|
+
type LettaClient = InstanceType<typeof Letta>;
|
|
39
|
+
|
|
40
|
+
interface GetMessagesOptions {
|
|
41
|
+
startDate?: string;
|
|
42
|
+
endDate?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
agentId?: string;
|
|
45
|
+
after?: string;
|
|
46
|
+
before?: string;
|
|
47
|
+
order?: "asc" | "desc";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get API key from env var or settings file
|
|
52
|
+
*/
|
|
53
|
+
function getApiKey(): string {
|
|
54
|
+
// First check env var (set by CLI's getShellEnv)
|
|
55
|
+
if (process.env.LETTA_API_KEY) {
|
|
56
|
+
return process.env.LETTA_API_KEY;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fall back to settings file
|
|
60
|
+
const settingsPath = join(homedir(), ".letta", "settings.json");
|
|
61
|
+
try {
|
|
62
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
63
|
+
if (settings.env?.LETTA_API_KEY) {
|
|
64
|
+
return settings.env.LETTA_API_KEY;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Settings file doesn't exist or is invalid
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(
|
|
71
|
+
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get agent ID from CLI arg, env var, or throw
|
|
77
|
+
*/
|
|
78
|
+
function getAgentId(cliArg?: string): string {
|
|
79
|
+
// CLI arg takes precedence
|
|
80
|
+
if (cliArg) return cliArg;
|
|
81
|
+
|
|
82
|
+
// Then env var (set by CLI's getShellEnv)
|
|
83
|
+
if (process.env.LETTA_AGENT_ID) {
|
|
84
|
+
return process.env.LETTA_AGENT_ID;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(
|
|
88
|
+
"No agent ID provided. Use --agent-id or ensure LETTA_AGENT_ID env var is set.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a Letta client with auth from env/settings
|
|
94
|
+
*/
|
|
95
|
+
function createClient(): LettaClient {
|
|
96
|
+
return new Letta({ apiKey: getApiKey() });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get messages from an agent, optionally filtered by date range
|
|
101
|
+
* @param client - Letta client instance
|
|
102
|
+
* @param options - Options for filtering
|
|
103
|
+
* @returns Array of messages in chronological order
|
|
104
|
+
*/
|
|
105
|
+
export async function getMessages(
|
|
106
|
+
client: LettaClient,
|
|
107
|
+
options: GetMessagesOptions = {},
|
|
108
|
+
): Promise<unknown[]> {
|
|
109
|
+
const agentId = getAgentId(options.agentId);
|
|
110
|
+
const limit = options.limit ?? 20;
|
|
111
|
+
|
|
112
|
+
// Fetch messages from the agent
|
|
113
|
+
const response = await client.agents.messages.list(agentId, {
|
|
114
|
+
limit,
|
|
115
|
+
after: options.after,
|
|
116
|
+
before: options.before,
|
|
117
|
+
order: options.order,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const messages = response.items ?? [];
|
|
121
|
+
|
|
122
|
+
// Client-side date filtering if specified
|
|
123
|
+
if (options.startDate || options.endDate) {
|
|
124
|
+
const startTime = options.startDate
|
|
125
|
+
? new Date(options.startDate).getTime()
|
|
126
|
+
: 0;
|
|
127
|
+
const endTime = options.endDate
|
|
128
|
+
? new Date(options.endDate).getTime()
|
|
129
|
+
: Number.POSITIVE_INFINITY;
|
|
130
|
+
|
|
131
|
+
const filtered = messages.filter((msg) => {
|
|
132
|
+
// Messages use 'date' field, not 'created_at'
|
|
133
|
+
if (!("date" in msg) || !msg.date) return true;
|
|
134
|
+
const msgTime = new Date(msg.date).getTime();
|
|
135
|
+
return msgTime >= startTime && msgTime <= endTime;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Sort chronologically (oldest first)
|
|
139
|
+
return filtered.sort((a, b) => {
|
|
140
|
+
const aDate = "date" in a && a.date ? new Date(a.date).getTime() : 0;
|
|
141
|
+
const bDate = "date" in b && b.date ? new Date(b.date).getTime() : 0;
|
|
142
|
+
return aDate - bDate;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sort chronologically (oldest first)
|
|
147
|
+
return [...messages].sort((a, b) => {
|
|
148
|
+
const aDate = "date" in a && a.date ? new Date(a.date).getTime() : 0;
|
|
149
|
+
const bDate = "date" in b && b.date ? new Date(b.date).getTime() : 0;
|
|
150
|
+
return aDate - bDate;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseArgs(args: string[]): GetMessagesOptions {
|
|
155
|
+
const options: GetMessagesOptions = {};
|
|
156
|
+
|
|
157
|
+
const startDateIndex = args.indexOf("--start-date");
|
|
158
|
+
if (startDateIndex !== -1 && startDateIndex + 1 < args.length) {
|
|
159
|
+
options.startDate = args[startDateIndex + 1];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const endDateIndex = args.indexOf("--end-date");
|
|
163
|
+
if (endDateIndex !== -1 && endDateIndex + 1 < args.length) {
|
|
164
|
+
options.endDate = args[endDateIndex + 1];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const limitIndex = args.indexOf("--limit");
|
|
168
|
+
if (limitIndex !== -1 && limitIndex + 1 < args.length) {
|
|
169
|
+
const limit = Number.parseInt(args[limitIndex + 1] as string, 10);
|
|
170
|
+
if (!Number.isNaN(limit)) {
|
|
171
|
+
options.limit = limit;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const agentIdIndex = args.indexOf("--agent-id");
|
|
176
|
+
if (agentIdIndex !== -1 && agentIdIndex + 1 < args.length) {
|
|
177
|
+
options.agentId = args[agentIdIndex + 1];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const afterIndex = args.indexOf("--after");
|
|
181
|
+
if (afterIndex !== -1 && afterIndex + 1 < args.length) {
|
|
182
|
+
options.after = args[afterIndex + 1];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const beforeIndex = args.indexOf("--before");
|
|
186
|
+
if (beforeIndex !== -1 && beforeIndex + 1 < args.length) {
|
|
187
|
+
options.before = args[beforeIndex + 1];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const orderIndex = args.indexOf("--order");
|
|
191
|
+
if (orderIndex !== -1 && orderIndex + 1 < args.length) {
|
|
192
|
+
const order = args[orderIndex + 1] as string;
|
|
193
|
+
if (order === "asc" || order === "desc") {
|
|
194
|
+
options.order = order;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return options;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// CLI entry point - check if this file is being run directly
|
|
202
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
203
|
+
if (isMainModule) {
|
|
204
|
+
(async () => {
|
|
205
|
+
try {
|
|
206
|
+
const options = parseArgs(process.argv.slice(2));
|
|
207
|
+
const client = createClient();
|
|
208
|
+
const result = await getMessages(client, options);
|
|
209
|
+
console.log(JSON.stringify(result, null, 2));
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(
|
|
212
|
+
"Error:",
|
|
213
|
+
error instanceof Error ? error.message : String(error),
|
|
214
|
+
);
|
|
215
|
+
console.error(`
|
|
216
|
+
Usage: npx tsx get-messages.ts [options]
|
|
217
|
+
|
|
218
|
+
Options:
|
|
219
|
+
--after <message-id> Cursor: get messages after this ID
|
|
220
|
+
--before <message-id> Cursor: get messages before this ID
|
|
221
|
+
--order <asc|desc> Sort order (default: desc = newest first)
|
|
222
|
+
--limit <n> Max results (default: 20)
|
|
223
|
+
--agent-id <id> Explicit agent ID (overrides LETTA_AGENT_ID env var)
|
|
224
|
+
--start-date <date> Client-side filter: after this date (ISO format)
|
|
225
|
+
--end-date <date> Client-side filter: before this date (ISO format)
|
|
226
|
+
`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
}
|