@letta-ai/letta-code 0.27.24 → 0.27.26
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/README.md +15 -0
- package/dist/types/types/protocol_v2.d.ts +16 -0
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/letta.js +2446 -1037
- package/package.json +2 -1
- package/scripts/check-skill-frontmatter.js +116 -0
- package/scripts/check.js +1 -0
- package/skills/messaging-agents/SKILL.md +44 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.26",
|
|
4
4
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.0",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"check:filename-casing": "node scripts/check-filename-casing.js",
|
|
98
98
|
"check:test-mock-isolation": "bun run scripts/check-test-mock-isolation.js",
|
|
99
99
|
"check:test-coverage": "node scripts/check-test-coverage.cjs",
|
|
100
|
+
"check:skill-frontmatter": "node scripts/check-skill-frontmatter.js",
|
|
100
101
|
"check:bundled-skill-scripts": "node scripts/check-bundled-skill-scripts.js",
|
|
101
102
|
"check": "bun run scripts/check.js",
|
|
102
103
|
"dev": "node scripts/dev.cjs",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const isStagedOnly = process.argv.includes("--staged");
|
|
7
|
+
|
|
8
|
+
function git(args) {
|
|
9
|
+
const result = spawnSync("git", args, {
|
|
10
|
+
cwd: process.cwd(),
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
(result.stderr || result.stdout || "git command failed").trim(),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result.stdout;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function splitNul(output) {
|
|
25
|
+
return output.split("\0").filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSkillMarkdownFile(file) {
|
|
29
|
+
return file.split(/[\\/]/).pop() === "SKILL.md";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getCandidateFiles() {
|
|
33
|
+
if (isStagedOnly) {
|
|
34
|
+
return splitNul(
|
|
35
|
+
git(["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"]),
|
|
36
|
+
).filter(isSkillMarkdownFile);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return splitNul(git(["ls-files", "-z"])).filter(isSkillMarkdownFile);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readStagedFile(file) {
|
|
43
|
+
return git(["show", `:${file}`]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readCurrentFile(file) {
|
|
47
|
+
if (!existsSync(file)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return readFileSync(file, "utf8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checkSkillFrontmatterName(content) {
|
|
54
|
+
const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
55
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
56
|
+
if (!match) {
|
|
57
|
+
return "missing YAML frontmatter";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const frontmatter = match[1] ?? "";
|
|
61
|
+
const nameLine = frontmatter
|
|
62
|
+
.split("\n")
|
|
63
|
+
.find((line) => /^\s*name\s*:/.test(line));
|
|
64
|
+
|
|
65
|
+
if (!nameLine) {
|
|
66
|
+
return "missing name frontmatter";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nameValue = nameLine.replace(/^\s*name\s*:\s*/, "").trim();
|
|
70
|
+
if (!nameValue) {
|
|
71
|
+
return "empty name frontmatter";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let violations = [];
|
|
78
|
+
let files = [];
|
|
79
|
+
try {
|
|
80
|
+
files = getCandidateFiles();
|
|
81
|
+
violations = files.flatMap((file) => {
|
|
82
|
+
const content = isStagedOnly ? readStagedFile(file) : readCurrentFile(file);
|
|
83
|
+
if (content === null) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const reason = checkSkillFrontmatterName(content);
|
|
88
|
+
return reason ? [{ file, reason }] : [];
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(
|
|
92
|
+
`Failed to check skill frontmatter: ${error instanceof Error ? error.message : String(error)}`,
|
|
93
|
+
);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (violations.length > 0) {
|
|
98
|
+
console.error("\n❌ Skill frontmatter violations found:\n");
|
|
99
|
+
for (const violation of violations) {
|
|
100
|
+
console.error(`${violation.file}`);
|
|
101
|
+
console.error(` ${violation.reason}`);
|
|
102
|
+
console.error(
|
|
103
|
+
" ↳ Add a non-empty `name:` field to SKILL.md frontmatter.\n",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
console.error(
|
|
107
|
+
`Found ${violations.length} skill frontmatter violation${violations.length === 1 ? "" : "s"}.`,
|
|
108
|
+
);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!isStagedOnly) {
|
|
113
|
+
console.log(
|
|
114
|
+
`✅ Skill frontmatter name headers present (${files.length} SKILL.md file${files.length === 1 ? "" : "s"}).`,
|
|
115
|
+
);
|
|
116
|
+
}
|
package/scripts/check.js
CHANGED
|
@@ -19,6 +19,7 @@ const checks = [
|
|
|
19
19
|
{ name: "filename casing", script: ["check:filename-casing"] },
|
|
20
20
|
{ name: "test mock isolation", script: ["check:test-mock-isolation"] },
|
|
21
21
|
{ name: "test coverage", script: ["check:test-coverage"] },
|
|
22
|
+
{ name: "skill frontmatter", script: ["check:skill-frontmatter"] },
|
|
22
23
|
{ name: "bundled skill scripts", script: ["check:bundled-skill-scripts"] },
|
|
23
24
|
{ name: "biome", script: ["lint"] },
|
|
24
25
|
{ name: "typescript", script: ["typecheck"] },
|
|
@@ -66,11 +66,24 @@ Results include `agent_id` for each matching message.
|
|
|
66
66
|
letta -p --from-agent $LETTA_AGENT_ID --agent <id> "message text"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
When no `--environment` is specified, the target agent will run in the same
|
|
70
|
+
environment as the caller agent.
|
|
71
|
+
|
|
72
|
+
To route the target agent turn through a specific remote/local environment:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
letta -p --from-agent $LETTA_AGENT_ID \
|
|
76
|
+
--agent <id> \
|
|
77
|
+
--environment <name-or-device-id-or-connection-id> \
|
|
78
|
+
"message text"
|
|
79
|
+
```
|
|
80
|
+
|
|
69
81
|
**Arguments:**
|
|
70
82
|
| Arg | Required | Description |
|
|
71
83
|
|-----|----------|-------------|
|
|
72
84
|
| `--agent <id>` | Yes | Target agent ID to message |
|
|
73
85
|
| `--from-agent <id>` | Yes | Sender agent ID (injects agent-to-agent system reminder) |
|
|
86
|
+
| `--environment <selector>` | No | Route through an online environment by connection name, device ID, or connection ID |
|
|
74
87
|
| `"message text"` | Yes | Message body (positional after flags) |
|
|
75
88
|
|
|
76
89
|
**Example:**
|
|
@@ -96,6 +109,35 @@ letta -p --from-agent $LETTA_AGENT_ID \
|
|
|
96
109
|
letta -p --from-agent $LETTA_AGENT_ID --conversation <id> "message text"
|
|
97
110
|
```
|
|
98
111
|
|
|
112
|
+
Add `--environment <selector>` to continue the conversation on a specific environment.
|
|
113
|
+
|
|
114
|
+
### Discovering Environments
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
letta environments list --online-only
|
|
118
|
+
# alias:
|
|
119
|
+
letta envs list --online-only
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `connectionName`, `deviceId`, or `connectionId` from the JSON output as the
|
|
123
|
+
`--environment` selector. If a name is ambiguous, prefer `deviceId` or
|
|
124
|
+
`connectionId`. In `environments list`, the current local runtime is marked with
|
|
125
|
+
`"isCurrent": true`.
|
|
126
|
+
|
|
127
|
+
To force the target agent onto the current registered Letta Code environment,
|
|
128
|
+
resolve the current environment and pass its `connectionId`:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
CURRENT_ENV=$(letta environments current | jq -r .connectionId)
|
|
132
|
+
letta -p --from-agent $LETTA_AGENT_ID \
|
|
133
|
+
--agent agent-abc123 \
|
|
134
|
+
--environment "$CURRENT_ENV" \
|
|
135
|
+
"Run on my same machine/environment."
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Omit `--environment` when you want the target agent to run in the same
|
|
139
|
+
environment as the caller agent.
|
|
140
|
+
|
|
99
141
|
**Arguments:**
|
|
100
142
|
| Arg | Required | Description |
|
|
101
143
|
|-----|----------|-------------|
|
|
@@ -112,7 +154,8 @@ letta -p --from-agent $LETTA_AGENT_ID \
|
|
|
112
154
|
|
|
113
155
|
## Understanding the Response
|
|
114
156
|
|
|
115
|
-
-
|
|
157
|
+
- Text-mode scripts return only the **final assistant message** (not tool calls, reasoning, or metadata)
|
|
158
|
+
- JSON and stream-json responses include `agent_id`, `conversation_id`, and `environment.source` so you can continue the same conversation/runtime. Environment-routed turns also include `environment.id`, `connection_id`, `device_id`, and `name`.
|
|
116
159
|
- The target agent may use tools, think, and reason - but you only see their final response
|
|
117
160
|
- To see the full conversation transcript (including tool calls), use the `searching-messages` skill with `letta messages list --agent <id>` targeting the other agent
|
|
118
161
|
|