@letta-ai/letta-code 0.23.11 → 0.24.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/README.md +7 -5
- package/letta.js +2400 -1115
- package/package.json +1 -1
- package/skills/initializing-memory/SKILL.md +4 -4
- package/skills/messaging-agents/SKILL.md +2 -2
- package/skills/modifying-letta-code/SKILL.md +270 -0
- package/skills/modifying-letta-code/scripts/add_hook.py +223 -0
- package/skills/modifying-letta-code/scripts/add_permission.py +135 -0
- package/skills/modifying-letta-code/scripts/show_config.py +211 -0
- package/skills/syncing-memory-filesystem/SKILL.md +31 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Show the merged Letta Code harness configuration.
|
|
4
|
+
|
|
5
|
+
Displays permissions, hooks, and per-agent settings across user/project/local
|
|
6
|
+
scopes with their source scope annotated.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 show_config.py
|
|
10
|
+
python3 show_config.py --cwd /path/to/project
|
|
11
|
+
python3 show_config.py --json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_settings_paths(working_directory: str) -> list[tuple[str, Path]]:
|
|
21
|
+
"""Return (scope, path) in precedence order (lowest to highest)."""
|
|
22
|
+
return [
|
|
23
|
+
("user", Path.home() / ".letta" / "settings.json"),
|
|
24
|
+
("project", Path(working_directory) / ".letta" / "settings.json"),
|
|
25
|
+
("local", Path(working_directory) / ".letta" / "settings.local.json"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_settings(path: Path) -> dict:
|
|
30
|
+
if not path.exists():
|
|
31
|
+
return {}
|
|
32
|
+
try:
|
|
33
|
+
with open(path) as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
except (json.JSONDecodeError, IOError):
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def format_permissions(
|
|
40
|
+
all_settings: list[tuple[str, dict]], as_json: bool
|
|
41
|
+
) -> dict | None:
|
|
42
|
+
"""Collect permissions from all scopes with sources."""
|
|
43
|
+
rules: dict[str, list[tuple[str, str]]] = {"allow": [], "deny": [], "ask": []}
|
|
44
|
+
for scope, settings in all_settings:
|
|
45
|
+
perms = settings.get("permissions", {})
|
|
46
|
+
for rule_type in ["allow", "deny", "ask"]:
|
|
47
|
+
for rule in perms.get(rule_type, []):
|
|
48
|
+
rules[rule_type].append((rule, scope))
|
|
49
|
+
|
|
50
|
+
if as_json:
|
|
51
|
+
return {
|
|
52
|
+
t: [{"rule": r, "scope": s} for r, s in rules[t]] for t in rules if rules[t]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
total = sum(len(v) for v in rules.values())
|
|
56
|
+
print("=" * 60)
|
|
57
|
+
print(f"PERMISSIONS ({total} rules)")
|
|
58
|
+
print("=" * 60)
|
|
59
|
+
if total == 0:
|
|
60
|
+
print(" (none)")
|
|
61
|
+
else:
|
|
62
|
+
for rule_type in ["allow", "deny", "ask"]:
|
|
63
|
+
if rules[rule_type]:
|
|
64
|
+
print(f"\n {rule_type.upper()}:")
|
|
65
|
+
for rule, scope in rules[rule_type]:
|
|
66
|
+
print(f" [{scope:7}] {rule}")
|
|
67
|
+
print()
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_hooks(all_settings: list[tuple[str, dict]], as_json: bool) -> dict | None:
|
|
72
|
+
"""Collect hooks from all scopes with sources."""
|
|
73
|
+
collected: dict[str, list[tuple[str, dict]]] = {}
|
|
74
|
+
for scope, settings in all_settings:
|
|
75
|
+
hooks = settings.get("hooks", {})
|
|
76
|
+
if not isinstance(hooks, dict):
|
|
77
|
+
continue
|
|
78
|
+
for event, entries in hooks.items():
|
|
79
|
+
if event == "disabled":
|
|
80
|
+
continue
|
|
81
|
+
if not isinstance(entries, list):
|
|
82
|
+
continue
|
|
83
|
+
collected.setdefault(event, []).extend((scope, e) for e in entries)
|
|
84
|
+
|
|
85
|
+
if as_json:
|
|
86
|
+
return {
|
|
87
|
+
event: [
|
|
88
|
+
{"scope": scope, **entry}
|
|
89
|
+
for scope, entry in entries
|
|
90
|
+
]
|
|
91
|
+
for event, entries in collected.items()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
total_groups = sum(len(v) for v in collected.values())
|
|
95
|
+
print("=" * 60)
|
|
96
|
+
print(f"HOOKS ({len(collected)} events, {total_groups} groups)")
|
|
97
|
+
print("=" * 60)
|
|
98
|
+
if not collected:
|
|
99
|
+
print(" (none)")
|
|
100
|
+
else:
|
|
101
|
+
for event, entries in sorted(collected.items()):
|
|
102
|
+
print(f"\n {event}:")
|
|
103
|
+
for scope, entry in entries:
|
|
104
|
+
matcher = entry.get("matcher", "(no matcher)")
|
|
105
|
+
hook_list = entry.get("hooks", [])
|
|
106
|
+
for h in hook_list:
|
|
107
|
+
htype = h.get("type", "?")
|
|
108
|
+
detail = h.get("command") or h.get("prompt", "")
|
|
109
|
+
detail = (
|
|
110
|
+
(detail[:60] + "...") if len(detail) > 60 else detail
|
|
111
|
+
)
|
|
112
|
+
print(f" [{scope:7}] matcher={matcher:15} {htype}: {detail}")
|
|
113
|
+
print()
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def format_agents(all_settings: list[tuple[str, dict]], as_json: bool) -> dict | None:
|
|
118
|
+
"""Collect per-agent settings (only from user settings.json)."""
|
|
119
|
+
agents = []
|
|
120
|
+
for scope, settings in all_settings:
|
|
121
|
+
for a in settings.get("agents", []):
|
|
122
|
+
agents.append({"scope": scope, **a})
|
|
123
|
+
|
|
124
|
+
if as_json:
|
|
125
|
+
return agents
|
|
126
|
+
|
|
127
|
+
print("=" * 60)
|
|
128
|
+
print(f"PER-AGENT SETTINGS ({len(agents)} entries)")
|
|
129
|
+
print("=" * 60)
|
|
130
|
+
if not agents:
|
|
131
|
+
print(" (none)")
|
|
132
|
+
else:
|
|
133
|
+
for a in agents:
|
|
134
|
+
scope = a.get("scope", "?")
|
|
135
|
+
aid = a.get("agentId", "?")
|
|
136
|
+
print(f"\n [{scope:7}] {aid}")
|
|
137
|
+
for k in ("pinned", "memfs", "toolset", "systemPromptPreset", "baseUrl"):
|
|
138
|
+
if k in a:
|
|
139
|
+
val = a[k]
|
|
140
|
+
if isinstance(val, dict):
|
|
141
|
+
val = json.dumps(val)
|
|
142
|
+
print(f" {k}: {val}")
|
|
143
|
+
print()
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_settings_files(working_directory: str) -> None:
|
|
148
|
+
print("=" * 60)
|
|
149
|
+
print("SETTINGS FILES")
|
|
150
|
+
print("=" * 60)
|
|
151
|
+
for scope, path in get_settings_paths(working_directory):
|
|
152
|
+
exists = "✓" if path.exists() else "✗"
|
|
153
|
+
print(f" {exists} [{scope:7}] {path}")
|
|
154
|
+
print()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main():
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
description="Show Letta Code harness configuration (permissions, hooks, agents)"
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--cwd",
|
|
163
|
+
default=os.getcwd(),
|
|
164
|
+
help="Working directory for project/local scope (default: cwd)",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--section",
|
|
169
|
+
choices=["permissions", "hooks", "agents", "all"],
|
|
170
|
+
default="all",
|
|
171
|
+
help="Which section to show (default: all)",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
args = parser.parse_args()
|
|
175
|
+
|
|
176
|
+
all_settings = [
|
|
177
|
+
(scope, load_settings(path))
|
|
178
|
+
for scope, path in get_settings_paths(args.cwd)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if args.json:
|
|
182
|
+
output: dict = {}
|
|
183
|
+
if args.section in ("permissions", "all"):
|
|
184
|
+
perms = format_permissions(all_settings, as_json=True)
|
|
185
|
+
if perms:
|
|
186
|
+
output["permissions"] = perms
|
|
187
|
+
if args.section in ("hooks", "all"):
|
|
188
|
+
hooks = format_hooks(all_settings, as_json=True)
|
|
189
|
+
if hooks:
|
|
190
|
+
output["hooks"] = hooks
|
|
191
|
+
if args.section in ("agents", "all"):
|
|
192
|
+
agents = format_agents(all_settings, as_json=True)
|
|
193
|
+
if agents:
|
|
194
|
+
output["agents"] = agents
|
|
195
|
+
print(json.dumps(output, indent=2))
|
|
196
|
+
else:
|
|
197
|
+
print(f"\nLetta Code Harness Configuration")
|
|
198
|
+
print(f"Working directory: {args.cwd}\n")
|
|
199
|
+
if args.section == "all":
|
|
200
|
+
format_settings_files(args.cwd)
|
|
201
|
+
if args.section in ("permissions", "all"):
|
|
202
|
+
format_permissions(all_settings, as_json=False)
|
|
203
|
+
if args.section in ("hooks", "all"):
|
|
204
|
+
format_hooks(all_settings, as_json=False)
|
|
205
|
+
if args.section in ("agents", "all"):
|
|
206
|
+
format_agents(all_settings, as_json=False)
|
|
207
|
+
print("Precedence (highest to lowest): local > project > user\n")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
main()
|
|
@@ -21,8 +21,10 @@ When memfs is enabled, the Letta Code CLI automatically:
|
|
|
21
21
|
2. Clones the repo into `~/.letta/agents/<agent-id>/memory/` (git root is the memory directory)
|
|
22
22
|
3. Configures a **local** credential helper in `memory/.git/config` (so `git push`/`git pull` work without auth ceremony)
|
|
23
23
|
4. Installs a **pre-commit hook** that validates frontmatter before each commit (see below)
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
24
|
+
5. Installs a **post-commit hook** that pushes commits to an optional additional remote (see "Additional memory-repository remote" below)
|
|
25
|
+
6. Sets canonical local git identity (`letta.agentId`, `user.name`, `user.email`) so direct `git commit` from the agent's shell attributes correctly to the agent — not the operator's global git identity
|
|
26
|
+
7. On subsequent startups: pulls latest changes, reconfigures credentials, hooks, and identity (self-healing)
|
|
27
|
+
8. During sessions: periodically checks `git status` and reminds you (the agent) to commit/push if dirty
|
|
26
28
|
|
|
27
29
|
If any of these steps fail, you can replicate them manually using the sections below.
|
|
28
30
|
|
|
@@ -83,6 +85,33 @@ Block content goes here.
|
|
|
83
85
|
|
|
84
86
|
If the hook rejects a commit, read the error message — it tells you exactly which file and which rule was violated. Fix the file and retry.
|
|
85
87
|
|
|
88
|
+
## Additional Memory-Repository Remote
|
|
89
|
+
|
|
90
|
+
In addition to pushing to the Letta server, you can push every commit to a second git remote — e.g. a private GitHub repo — so you have a backup or a copy you can browse with regular tools.
|
|
91
|
+
|
|
92
|
+
**Via the slash command (recommended):**
|
|
93
|
+
```
|
|
94
|
+
/memory-repository set git@github.com:you/my-memory.git
|
|
95
|
+
/memory-repository status
|
|
96
|
+
/memory-repository push # force a push now, e.g. after a network failure
|
|
97
|
+
/memory-repository unset # stop pushing
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**How it works:**
|
|
101
|
+
- `/memory-repository set <url>` writes the URL to `letta.memoryRepository.url` in the memfs repo's local `.git/config` and installs a `post-commit` hook.
|
|
102
|
+
- After every commit, the hook reads `letta.memoryRepository.url` and asynchronously pushes to it in the background. Commits are never blocked by push failures.
|
|
103
|
+
- Push output and exit codes are appended to `.git/memory-repository-push.log` — visible via `/memory-repository status`.
|
|
104
|
+
- The setting is **per-repo**, so each agent on a machine has its own independent configuration.
|
|
105
|
+
|
|
106
|
+
**Auth:** uses your existing git credentials — SSH keys, credential helpers, or tokens in the URL. Letta does not store tokens for this feature. If you're pushing to GitHub, SSH is easiest.
|
|
107
|
+
|
|
108
|
+
**Manual equivalent (without the slash command):**
|
|
109
|
+
```bash
|
|
110
|
+
cd ~/.letta/agents/<agent-id>/memory
|
|
111
|
+
git config --local letta.memoryRepository.url git@github.com:you/my-memory.git
|
|
112
|
+
# Hook is installed automatically by the CLI on startup; no manual install needed.
|
|
113
|
+
```
|
|
114
|
+
|
|
86
115
|
## Clone Agent Memory
|
|
87
116
|
|
|
88
117
|
```bash
|