@letta-ai/letta-code 0.11.2-next.3 → 0.12.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/package.json
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acquiring-skills
|
|
3
|
+
description: Guide for safely discovering and installing skills from external repositories. Use when a user asks for something where a specialized skill likely exists (browser testing, PDF processing, document generation, etc.) and you want to bootstrap your understanding rather than starting from scratch.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Acquiring New Skills
|
|
7
|
+
|
|
8
|
+
This skill teaches you how to safely discover and install skills from external sources.
|
|
9
|
+
|
|
10
|
+
## SAFETY - READ THIS FIRST
|
|
11
|
+
|
|
12
|
+
Skills can contain:
|
|
13
|
+
- **Markdown files** (.md) - Risk: prompt injection, misleading instructions
|
|
14
|
+
- **Scripts** (Python, TypeScript, Bash) - Risk: malicious code execution
|
|
15
|
+
|
|
16
|
+
### Trusted Sources (no user approval needed for download)
|
|
17
|
+
- `https://github.com/letta-ai/skills` - Letta's community skills
|
|
18
|
+
- `https://github.com/anthropics/skills` - Anthropic's official skills
|
|
19
|
+
|
|
20
|
+
### Untrusted Sources (ALWAYS verify with user)
|
|
21
|
+
For ANY source other than letta-ai or anthropics:
|
|
22
|
+
1. Ask the user before downloading
|
|
23
|
+
2. Explain where the skill comes from
|
|
24
|
+
3. Get explicit approval
|
|
25
|
+
|
|
26
|
+
### Script Safety
|
|
27
|
+
Even for skills from trusted sources, ALWAYS:
|
|
28
|
+
1. Read and inspect any scripts before executing them
|
|
29
|
+
2. Understand what the script does
|
|
30
|
+
3. Be wary of network calls, file operations, or system commands
|
|
31
|
+
|
|
32
|
+
## When to Use This Skill
|
|
33
|
+
|
|
34
|
+
**DO use** when:
|
|
35
|
+
- User asks for something where a skill likely exists (e.g., "help me test this webapp", "generate a PDF report")
|
|
36
|
+
- You think "there's probably a skill that would bootstrap my understanding"
|
|
37
|
+
- User explicitly asks about available skills or extending capabilities
|
|
38
|
+
|
|
39
|
+
**DON'T use** for:
|
|
40
|
+
- General coding tasks you can already handle
|
|
41
|
+
- Simple bug fixes or feature implementations
|
|
42
|
+
- Tasks where you have sufficient knowledge
|
|
43
|
+
|
|
44
|
+
## Ask Before Searching (Interactive Mode)
|
|
45
|
+
|
|
46
|
+
If you recognize a task that might have an associated skill, **ask the user first**:
|
|
47
|
+
|
|
48
|
+
> "This sounds like something where a community skill might help (e.g., webapp testing with Playwright). Would you like me to look for available skills in the Letta or Anthropic repositories? This might take a minute, or I can start coding right away if you prefer."
|
|
49
|
+
|
|
50
|
+
The user may prefer to start immediately rather than wait for skill discovery.
|
|
51
|
+
|
|
52
|
+
Only proceed with skill acquisition if the user agrees.
|
|
53
|
+
|
|
54
|
+
## Skill Repositories
|
|
55
|
+
|
|
56
|
+
| Repository | Description |
|
|
57
|
+
|------------|-------------|
|
|
58
|
+
| https://github.com/letta-ai/skills | Community skills for Letta agents |
|
|
59
|
+
| https://github.com/anthropics/skills | Anthropic's official Agent Skills |
|
|
60
|
+
|
|
61
|
+
Browse these repositories to discover available skills. Check the README for skill listings.
|
|
62
|
+
|
|
63
|
+
## Installation Locations
|
|
64
|
+
|
|
65
|
+
| Location | Path | When to Use |
|
|
66
|
+
|----------|------|-------------|
|
|
67
|
+
| **Global** | `~/.letta/skills/<skill>/` | General-purpose skills useful across projects |
|
|
68
|
+
| **Project** | `.skills/<skill>/` | Project-specific skills |
|
|
69
|
+
|
|
70
|
+
**Rule**: If useful across multiple projects, install globally. If project-specific, install in `.skills/`.
|
|
71
|
+
|
|
72
|
+
## How to Download Skills
|
|
73
|
+
|
|
74
|
+
Skills are directories containing SKILL.md and optionally scripts/, references/, examples/.
|
|
75
|
+
|
|
76
|
+
### Method: Clone to /tmp, then copy
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# 1. Clone the repo (shallow)
|
|
80
|
+
git clone --depth 1 https://github.com/anthropics/skills /tmp/skills-temp
|
|
81
|
+
|
|
82
|
+
# 2. Copy the skill to your skills directory
|
|
83
|
+
# For global:
|
|
84
|
+
cp -r /tmp/skills-temp/skills/webapp-testing ~/.letta/skills/
|
|
85
|
+
# For project:
|
|
86
|
+
cp -r /tmp/skills-temp/skills/webapp-testing .skills/
|
|
87
|
+
|
|
88
|
+
# 3. Cleanup
|
|
89
|
+
rm -rf /tmp/skills-temp
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Alternative: rsync (preserves permissions)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone --depth 1 https://github.com/anthropics/skills /tmp/skills-temp
|
|
96
|
+
rsync -av /tmp/skills-temp/skills/webapp-testing/ ~/.letta/skills/webapp-testing/
|
|
97
|
+
rm -rf /tmp/skills-temp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Registering New Skills
|
|
101
|
+
|
|
102
|
+
After downloading, refresh the skills list:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Skill(command: "refresh")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This scans `~/.letta/skills/` and `.skills/` and updates your `skills` memory block.
|
|
109
|
+
|
|
110
|
+
## Complete Example
|
|
111
|
+
|
|
112
|
+
User asks: "Can you help me test my React app's UI?"
|
|
113
|
+
|
|
114
|
+
1. **Recognize opportunity**: Browser/webapp testing - likely has a skill
|
|
115
|
+
2. **Ask user**: "Would you like me to look for webapp testing skills, or start coding right away?"
|
|
116
|
+
3. **If user agrees, find skill**: Check anthropics/skills for webapp-testing
|
|
117
|
+
4. **Download** (trusted source):
|
|
118
|
+
```bash
|
|
119
|
+
git clone --depth 1 https://github.com/anthropics/skills /tmp/skills-temp
|
|
120
|
+
cp -r /tmp/skills-temp/skills/webapp-testing ~/.letta/skills/
|
|
121
|
+
rm -rf /tmp/skills-temp
|
|
122
|
+
```
|
|
123
|
+
5. **Refresh**: `Skill(command: "refresh")`
|
|
124
|
+
6. **Inspect scripts**: Read any .py or .ts files before using them
|
|
125
|
+
7. **Load**: `Skill(command: "load", skills: ["webapp-testing"])`
|
|
126
|
+
8. **Use**: Follow the skill's instructions for the user's task
|
|
@@ -29,7 +29,6 @@ Best for: Extracting sections, cleaning up messy content, selective migration.
|
|
|
29
29
|
|
|
30
30
|
Creates new blocks with the same content using `copy-block.ts`. After copying:
|
|
31
31
|
- You own the copy - changes don't sync
|
|
32
|
-
- Use `--label` flag if you already have a block with that label
|
|
33
32
|
- Best for: One-time migration, forking an agent
|
|
34
33
|
|
|
35
34
|
### 3. Share (Linked Blocks)
|
|
@@ -40,7 +39,31 @@ Attaches the same block to multiple agents using `attach-block.ts`. After sharin
|
|
|
40
39
|
- Can be read-only (target can read but not modify)
|
|
41
40
|
- Best for: Shared knowledge bases, synchronized state
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
## Handling Duplicate Label Errors
|
|
43
|
+
|
|
44
|
+
**You cannot have two blocks with the same label.** If you try to copy/attach a block and you already have one with that label, you'll get a `duplicate key value violates unique constraint` error.
|
|
45
|
+
|
|
46
|
+
**Solutions:**
|
|
47
|
+
|
|
48
|
+
1. **Use `--label` (copy only):** Rename the block when copying:
|
|
49
|
+
```bash
|
|
50
|
+
npx tsx <SKILL_DIR>/scripts/copy-block.ts --block-id <id> --label project-imported
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. **Use `--override` (copy or attach):** Automatically detach your existing block first:
|
|
54
|
+
```bash
|
|
55
|
+
npx tsx <SKILL_DIR>/scripts/copy-block.ts --block-id <id> --override
|
|
56
|
+
npx tsx <SKILL_DIR>/scripts/attach-block.ts --block-id <id> --override
|
|
57
|
+
```
|
|
58
|
+
If the operation fails, the original block is automatically reattached.
|
|
59
|
+
|
|
60
|
+
3. **Manual detach first:** Use the `memory` tool to detach your existing block:
|
|
61
|
+
```
|
|
62
|
+
memory(agent_state, "delete", path="/memories/<label>")
|
|
63
|
+
```
|
|
64
|
+
Then run the copy/attach script.
|
|
65
|
+
|
|
66
|
+
**Note:** `attach-block.ts` does NOT support `--label` because attached blocks keep their original label (they're shared, not copied).
|
|
44
67
|
|
|
45
68
|
## Workflow
|
|
46
69
|
|
|
@@ -92,8 +115,8 @@ All scripts are located in the `scripts/` directory and output raw API responses
|
|
|
92
115
|
| Script | Purpose | Args |
|
|
93
116
|
|--------|---------|------|
|
|
94
117
|
| `get-agent-blocks.ts` | Get blocks from an agent | `--agent-id` |
|
|
95
|
-
| `copy-block.ts` | Copy block to current agent | `--block-id`, optional `--label` |
|
|
96
|
-
| `attach-block.ts` | Attach existing block to current agent | `--block-id`, optional `--read-only` |
|
|
118
|
+
| `copy-block.ts` | Copy block to current agent | `--block-id`, optional `--label`, `--override` |
|
|
119
|
+
| `attach-block.ts` | Attach existing block to current agent | `--block-id`, optional `--read-only`, `--override` |
|
|
97
120
|
|
|
98
121
|
## Authentication
|
|
99
122
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* It reads agent ID from LETTA_AGENT_ID env var or --agent-id arg.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* npx tsx attach-block.ts --block-id <block-id> [--agent-id <agent-id>] [--read-only]
|
|
10
|
+
* npx tsx attach-block.ts --block-id <block-id> [--agent-id <agent-id>] [--read-only] [--override]
|
|
11
11
|
*
|
|
12
12
|
* This attaches an existing block to another agent, making it shared.
|
|
13
13
|
* Changes to the block will be visible to all agents that have it attached.
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* Options:
|
|
16
16
|
* --agent-id Target agent ID (overrides LETTA_AGENT_ID env var)
|
|
17
17
|
* --read-only Target agent can read but not modify the block
|
|
18
|
+
* --override If you already have a block with the same label, detach it first
|
|
19
|
+
* (on error, the original block is reattached)
|
|
18
20
|
*
|
|
19
21
|
* Output:
|
|
20
22
|
* Raw API response from the attach operation
|
|
@@ -75,49 +77,109 @@ function createClient(): LettaClient {
|
|
|
75
77
|
return new Letta({ apiKey: getApiKey() });
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
interface AttachBlockResult {
|
|
81
|
+
attachResult: Awaited<ReturnType<LettaClient["agents"]["blocks"]["attach"]>>;
|
|
82
|
+
detachedBlock?: Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>;
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
/**
|
|
79
86
|
* Attach an existing block to the current agent (sharing it)
|
|
80
87
|
* @param client - Letta client instance
|
|
81
88
|
* @param blockId - The block ID to attach
|
|
82
|
-
* @param
|
|
83
|
-
* @param targetAgentId - Optional target agent ID (defaults to current agent)
|
|
89
|
+
* @param options - readOnly, targetAgentId, override (detach existing block with same label)
|
|
84
90
|
* @returns API response from the attach operation
|
|
85
91
|
*/
|
|
86
92
|
export async function attachBlock(
|
|
87
93
|
client: LettaClient,
|
|
88
94
|
blockId: string,
|
|
89
|
-
readOnly
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
95
|
+
options?: { readOnly?: boolean; targetAgentId?: string; override?: boolean },
|
|
96
|
+
): Promise<AttachBlockResult> {
|
|
97
|
+
const currentAgentId = getAgentId(options?.targetAgentId);
|
|
98
|
+
let detachedBlock:
|
|
99
|
+
| Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>
|
|
100
|
+
| undefined;
|
|
101
|
+
|
|
102
|
+
// If override is requested, check for existing block with same label and detach it
|
|
103
|
+
if (options?.override) {
|
|
104
|
+
// Get the block we're trying to attach to find its label
|
|
105
|
+
const sourceBlock = await client.blocks.retrieve(blockId);
|
|
106
|
+
const sourceLabel = sourceBlock.label;
|
|
107
|
+
|
|
108
|
+
// Get current agent's blocks to check for label conflict
|
|
109
|
+
const currentBlocksResponse =
|
|
110
|
+
await client.agents.blocks.list(currentAgentId);
|
|
111
|
+
// The response may be paginated or an array depending on SDK version
|
|
112
|
+
const currentBlocks = Array.isArray(currentBlocksResponse)
|
|
113
|
+
? currentBlocksResponse
|
|
114
|
+
: (currentBlocksResponse as { items?: unknown[] }).items || [];
|
|
115
|
+
const conflictingBlock = currentBlocks.find(
|
|
116
|
+
(b: { label?: string }) => b.label === sourceLabel,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (conflictingBlock) {
|
|
120
|
+
console.error(
|
|
121
|
+
`Detaching existing block with label "${sourceLabel}" (${conflictingBlock.id})...`,
|
|
122
|
+
);
|
|
123
|
+
detachedBlock = conflictingBlock;
|
|
124
|
+
try {
|
|
125
|
+
await client.agents.blocks.detach(conflictingBlock.id, {
|
|
126
|
+
agent_id: currentAgentId,
|
|
127
|
+
});
|
|
128
|
+
} catch (detachError) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Failed to detach existing block "${sourceLabel}": ${detachError instanceof Error ? detachError.message : String(detachError)}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Attempt to attach the new block
|
|
137
|
+
let attachResult: Awaited<ReturnType<typeof client.agents.blocks.attach>>;
|
|
138
|
+
try {
|
|
139
|
+
attachResult = await client.agents.blocks.attach(blockId, {
|
|
140
|
+
agent_id: currentAgentId,
|
|
141
|
+
});
|
|
142
|
+
} catch (attachError) {
|
|
143
|
+
// If attach failed and we detached a block, try to reattach it
|
|
144
|
+
if (detachedBlock) {
|
|
145
|
+
console.error(
|
|
146
|
+
`Attach failed, reattaching original block "${detachedBlock.label}"...`,
|
|
147
|
+
);
|
|
148
|
+
try {
|
|
149
|
+
await client.agents.blocks.attach(detachedBlock.id, {
|
|
150
|
+
agent_id: currentAgentId,
|
|
151
|
+
});
|
|
152
|
+
console.error("Original block reattached successfully.");
|
|
153
|
+
} catch {
|
|
154
|
+
console.error(
|
|
155
|
+
`WARNING: Failed to reattach original block! Block ID: ${detachedBlock.id}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw attachError;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If read-only is requested, note the limitation
|
|
163
|
+
if (options?.readOnly) {
|
|
104
164
|
console.warn(
|
|
105
165
|
"Note: read_only flag is set on the block itself, not per-agent. " +
|
|
106
166
|
"Use the block update API to set read_only if needed.",
|
|
107
167
|
);
|
|
108
168
|
}
|
|
109
169
|
|
|
110
|
-
return
|
|
170
|
+
return { attachResult, detachedBlock };
|
|
111
171
|
}
|
|
112
172
|
|
|
113
173
|
function parseArgs(args: string[]): {
|
|
114
174
|
blockId: string;
|
|
115
175
|
readOnly: boolean;
|
|
176
|
+
override: boolean;
|
|
116
177
|
agentId?: string;
|
|
117
178
|
} {
|
|
118
179
|
const blockIdIndex = args.indexOf("--block-id");
|
|
119
180
|
const agentIdIndex = args.indexOf("--agent-id");
|
|
120
181
|
const readOnly = args.includes("--read-only");
|
|
182
|
+
const override = args.includes("--override");
|
|
121
183
|
|
|
122
184
|
if (blockIdIndex === -1 || blockIdIndex + 1 >= args.length) {
|
|
123
185
|
throw new Error("Missing required argument: --block-id <block-id>");
|
|
@@ -126,6 +188,7 @@ function parseArgs(args: string[]): {
|
|
|
126
188
|
return {
|
|
127
189
|
blockId: args[blockIdIndex + 1] as string,
|
|
128
190
|
readOnly,
|
|
191
|
+
override,
|
|
129
192
|
agentId:
|
|
130
193
|
agentIdIndex !== -1 && agentIdIndex + 1 < args.length
|
|
131
194
|
? (args[agentIdIndex + 1] as string)
|
|
@@ -138,9 +201,15 @@ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
|
138
201
|
if (isMainModule) {
|
|
139
202
|
(async () => {
|
|
140
203
|
try {
|
|
141
|
-
const { blockId, readOnly, agentId } = parseArgs(
|
|
204
|
+
const { blockId, readOnly, override, agentId } = parseArgs(
|
|
205
|
+
process.argv.slice(2),
|
|
206
|
+
);
|
|
142
207
|
const client = createClient();
|
|
143
|
-
const result = await attachBlock(client, blockId,
|
|
208
|
+
const result = await attachBlock(client, blockId, {
|
|
209
|
+
readOnly,
|
|
210
|
+
override,
|
|
211
|
+
targetAgentId: agentId,
|
|
212
|
+
});
|
|
144
213
|
console.log(JSON.stringify(result, null, 2));
|
|
145
214
|
} catch (error) {
|
|
146
215
|
console.error(
|
|
@@ -152,7 +221,7 @@ if (isMainModule) {
|
|
|
152
221
|
error.message.includes("Missing required argument")
|
|
153
222
|
) {
|
|
154
223
|
console.error(
|
|
155
|
-
"\nUsage: npx tsx attach-block.ts --block-id <block-id> [--agent-id <agent-id>] [--read-only]",
|
|
224
|
+
"\nUsage: npx tsx attach-block.ts --block-id <block-id> [--agent-id <agent-id>] [--read-only] [--override]",
|
|
156
225
|
);
|
|
157
226
|
}
|
|
158
227
|
process.exit(1);
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* It reads agent ID from LETTA_AGENT_ID env var or --agent-id arg.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>]
|
|
10
|
+
* npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>] [--override]
|
|
11
11
|
*
|
|
12
12
|
* Options:
|
|
13
|
-
* --label Override the block label (
|
|
13
|
+
* --label Override the block label (useful to avoid duplicate label errors)
|
|
14
14
|
* --agent-id Target agent ID (overrides LETTA_AGENT_ID env var)
|
|
15
|
+
* --override If you already have a block with the same label, detach it first
|
|
16
|
+
* (on error, the original block is reattached)
|
|
15
17
|
*
|
|
16
18
|
* This creates a new block with the same content as the source block,
|
|
17
19
|
* then attaches it to the current agent. Changes to the new block
|
|
@@ -37,6 +39,7 @@ interface CopyBlockResult {
|
|
|
37
39
|
sourceBlock: Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>;
|
|
38
40
|
newBlock: Awaited<ReturnType<LettaClient["blocks"]["create"]>>;
|
|
39
41
|
attachResult: Awaited<ReturnType<LettaClient["agents"]["blocks"]["attach"]>>;
|
|
42
|
+
detachedBlock?: Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -86,44 +89,125 @@ function createClient(): LettaClient {
|
|
|
86
89
|
* Copy a block's content to a new block and attach to the current agent
|
|
87
90
|
* @param client - Letta client instance
|
|
88
91
|
* @param blockId - The source block ID to copy from
|
|
89
|
-
* @param options - Optional settings: labelOverride, targetAgentId
|
|
92
|
+
* @param options - Optional settings: labelOverride, targetAgentId, override
|
|
90
93
|
* @returns Object containing source block, new block, and attach result
|
|
91
94
|
*/
|
|
92
95
|
export async function copyBlock(
|
|
93
96
|
client: LettaClient,
|
|
94
97
|
blockId: string,
|
|
95
|
-
options?: {
|
|
98
|
+
options?: {
|
|
99
|
+
labelOverride?: string;
|
|
100
|
+
targetAgentId?: string;
|
|
101
|
+
override?: boolean;
|
|
102
|
+
},
|
|
96
103
|
): Promise<CopyBlockResult> {
|
|
97
|
-
// Get current agent ID (the agent calling this script) or use provided ID
|
|
98
104
|
const currentAgentId = getAgentId(options?.targetAgentId);
|
|
105
|
+
let detachedBlock:
|
|
106
|
+
| Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>
|
|
107
|
+
| undefined;
|
|
99
108
|
|
|
100
109
|
// 1. Get source block details
|
|
101
110
|
const sourceBlock = await client.blocks.retrieve(blockId);
|
|
111
|
+
const targetLabel =
|
|
112
|
+
options?.labelOverride || sourceBlock.label || "migrated-block";
|
|
102
113
|
|
|
103
|
-
// 2.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
// 2. If override is requested, check for existing block with same label and detach it
|
|
115
|
+
if (options?.override) {
|
|
116
|
+
const currentBlocksResponse =
|
|
117
|
+
await client.agents.blocks.list(currentAgentId);
|
|
118
|
+
// The response may be paginated or an array depending on SDK version
|
|
119
|
+
const currentBlocks = Array.isArray(currentBlocksResponse)
|
|
120
|
+
? currentBlocksResponse
|
|
121
|
+
: (currentBlocksResponse as { items?: unknown[] }).items || [];
|
|
122
|
+
const conflictingBlock = currentBlocks.find(
|
|
123
|
+
(b: { label?: string }) => b.label === targetLabel,
|
|
124
|
+
);
|
|
110
125
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
if (conflictingBlock) {
|
|
127
|
+
console.error(
|
|
128
|
+
`Detaching existing block with label "${targetLabel}" (${conflictingBlock.id})...`,
|
|
129
|
+
);
|
|
130
|
+
detachedBlock = conflictingBlock;
|
|
131
|
+
try {
|
|
132
|
+
await client.agents.blocks.detach(conflictingBlock.id, {
|
|
133
|
+
agent_id: currentAgentId,
|
|
134
|
+
});
|
|
135
|
+
} catch (detachError) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Failed to detach existing block "${targetLabel}": ${detachError instanceof Error ? detachError.message : String(detachError)}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
115
142
|
|
|
116
|
-
|
|
143
|
+
// 3. Create new block with same content
|
|
144
|
+
let newBlock: Awaited<ReturnType<LettaClient["blocks"]["create"]>>;
|
|
145
|
+
try {
|
|
146
|
+
newBlock = await client.blocks.create({
|
|
147
|
+
label: targetLabel,
|
|
148
|
+
value: sourceBlock.value,
|
|
149
|
+
description: sourceBlock.description || undefined,
|
|
150
|
+
limit: sourceBlock.limit,
|
|
151
|
+
});
|
|
152
|
+
} catch (createError) {
|
|
153
|
+
// If create failed and we detached a block, try to reattach it
|
|
154
|
+
if (detachedBlock) {
|
|
155
|
+
console.error(
|
|
156
|
+
`Create failed, reattaching original block "${detachedBlock.label}"...`,
|
|
157
|
+
);
|
|
158
|
+
try {
|
|
159
|
+
await client.agents.blocks.attach(detachedBlock.id, {
|
|
160
|
+
agent_id: currentAgentId,
|
|
161
|
+
});
|
|
162
|
+
console.error("Original block reattached successfully.");
|
|
163
|
+
} catch {
|
|
164
|
+
console.error(
|
|
165
|
+
`WARNING: Failed to reattach original block! Block ID: ${detachedBlock.id}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw createError;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 4. Attach new block to current agent
|
|
173
|
+
let attachResult: Awaited<ReturnType<typeof client.agents.blocks.attach>>;
|
|
174
|
+
try {
|
|
175
|
+
attachResult = await client.agents.blocks.attach(newBlock.id, {
|
|
176
|
+
agent_id: currentAgentId,
|
|
177
|
+
});
|
|
178
|
+
} catch (attachError) {
|
|
179
|
+
// If attach failed and we detached a block, try to reattach it
|
|
180
|
+
if (detachedBlock) {
|
|
181
|
+
console.error(
|
|
182
|
+
`Attach failed, reattaching original block "${detachedBlock.label}"...`,
|
|
183
|
+
);
|
|
184
|
+
try {
|
|
185
|
+
await client.agents.blocks.attach(detachedBlock.id, {
|
|
186
|
+
agent_id: currentAgentId,
|
|
187
|
+
});
|
|
188
|
+
console.error("Original block reattached successfully.");
|
|
189
|
+
} catch {
|
|
190
|
+
console.error(
|
|
191
|
+
`WARNING: Failed to reattach original block! Block ID: ${detachedBlock.id}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw attachError;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { sourceBlock, newBlock, attachResult, detachedBlock };
|
|
117
199
|
}
|
|
118
200
|
|
|
119
201
|
function parseArgs(args: string[]): {
|
|
120
202
|
blockId: string;
|
|
121
203
|
label?: string;
|
|
122
204
|
agentId?: string;
|
|
205
|
+
override: boolean;
|
|
123
206
|
} {
|
|
124
207
|
const blockIdIndex = args.indexOf("--block-id");
|
|
125
208
|
const labelIndex = args.indexOf("--label");
|
|
126
209
|
const agentIdIndex = args.indexOf("--agent-id");
|
|
210
|
+
const override = args.includes("--override");
|
|
127
211
|
|
|
128
212
|
if (blockIdIndex === -1 || blockIdIndex + 1 >= args.length) {
|
|
129
213
|
throw new Error("Missing required argument: --block-id <block-id>");
|
|
@@ -139,6 +223,7 @@ function parseArgs(args: string[]): {
|
|
|
139
223
|
agentIdIndex !== -1 && agentIdIndex + 1 < args.length
|
|
140
224
|
? (args[agentIdIndex + 1] as string)
|
|
141
225
|
: undefined,
|
|
226
|
+
override,
|
|
142
227
|
};
|
|
143
228
|
}
|
|
144
229
|
|
|
@@ -147,11 +232,14 @@ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
|
147
232
|
if (isMainModule) {
|
|
148
233
|
(async () => {
|
|
149
234
|
try {
|
|
150
|
-
const { blockId, label, agentId } = parseArgs(
|
|
235
|
+
const { blockId, label, agentId, override } = parseArgs(
|
|
236
|
+
process.argv.slice(2),
|
|
237
|
+
);
|
|
151
238
|
const client = createClient();
|
|
152
239
|
const result = await copyBlock(client, blockId, {
|
|
153
240
|
labelOverride: label,
|
|
154
241
|
targetAgentId: agentId,
|
|
242
|
+
override,
|
|
155
243
|
});
|
|
156
244
|
console.log(JSON.stringify(result, null, 2));
|
|
157
245
|
} catch (error) {
|
|
@@ -164,7 +252,7 @@ if (isMainModule) {
|
|
|
164
252
|
error.message.includes("Missing required argument")
|
|
165
253
|
) {
|
|
166
254
|
console.error(
|
|
167
|
-
"\nUsage: npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>]",
|
|
255
|
+
"\nUsage: npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>] [--override]",
|
|
168
256
|
);
|
|
169
257
|
}
|
|
170
258
|
process.exit(1);
|