@letta-ai/letta-code 0.28.10 → 0.28.12

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.
Files changed (27) hide show
  1. package/dist/agent-presets.js +268 -36
  2. package/dist/agent-presets.js.map +2 -2
  3. package/dist/channels-slack.js +2 -11
  4. package/dist/channels-slack.js.map +3 -3
  5. package/dist/types/agent/model-catalog.d.ts +0 -38
  6. package/dist/types/agent/model-catalog.d.ts.map +1 -1
  7. package/dist/types/channels/slack/progress.d.ts.map +1 -1
  8. package/dist/types/channels/types.d.ts +1 -0
  9. package/dist/types/channels/types.d.ts.map +1 -1
  10. package/letta.js +32445 -31820
  11. package/package.json +1 -1
  12. package/scripts/source-file-size-baseline.json +4 -4
  13. package/skills/creating-mods/references/events.md +4 -4
  14. package/skills/self-configuration/LICENSE +21 -0
  15. package/skills/self-configuration/SKILL.md +432 -0
  16. package/skills/self-configuration/references/api-patch-examples.md +161 -0
  17. package/skills/self-configuration/references/compaction-prompt-patterns.md +115 -0
  18. package/skills/self-configuration/references/model-settings.md +55 -0
  19. package/skills/self-configuration/scripts/add_permission.py +212 -0
  20. package/skills/self-configuration/scripts/show_config.py +370 -0
  21. package/skills/self-configuration/scripts/update-agent-settings.ts +384 -0
  22. package/skills/self-configuration/scripts/update-compaction-prompt.ts +227 -0
  23. package/skills/modifying-the-harness/SKILL.md +0 -263
  24. package/skills/modifying-the-harness/references/hooks.md +0 -261
  25. package/skills/modifying-the-harness/scripts/add_hook.py +0 -223
  26. package/skills/modifying-the-harness/scripts/add_permission.py +0 -136
  27. package/skills/modifying-the-harness/scripts/show_config.py +0 -212
@@ -1,223 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Add a hook to Letta Code settings.
4
-
5
- Examples:
6
- # Command hook for Bash tool calls
7
- python3 add_hook.py --event PreToolUse --matcher Bash \
8
- --type command --command 'echo "$TOOL_INPUT" >> audit.log' \
9
- --scope user
10
-
11
- # Prompt hook for pre-edit safety check
12
- python3 add_hook.py --event PreToolUse --matcher "Edit|Write" \
13
- --type prompt --prompt 'Is this safe? Input: $ARGUMENTS' \
14
- --model gpt-5.2 --scope project
15
-
16
- # Simple event hook (no matcher needed)
17
- python3 add_hook.py --event Stop \
18
- --type command --command 'say done' \
19
- --scope user
20
- """
21
-
22
- import argparse
23
- import json
24
- import os
25
- import sys
26
- from pathlib import Path
27
-
28
- TOOL_EVENTS = {"PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest"}
29
- SIMPLE_EVENTS = {
30
- "UserPromptSubmit",
31
- "Notification",
32
- "Stop",
33
- "SubagentStop",
34
- "PreCompact",
35
- "SessionStart",
36
- "SessionEnd",
37
- }
38
- ALL_EVENTS = TOOL_EVENTS | SIMPLE_EVENTS
39
-
40
- PROMPT_SUPPORTED = {
41
- "PreToolUse",
42
- "PostToolUse",
43
- "PostToolUseFailure",
44
- "PermissionRequest",
45
- "UserPromptSubmit",
46
- "Stop",
47
- "SubagentStop",
48
- }
49
-
50
-
51
- def get_settings_path(scope: str, working_directory: str) -> Path:
52
- if scope == "user":
53
- return Path.home() / ".letta" / "settings.json"
54
- elif scope == "project":
55
- return Path(working_directory) / ".letta" / "settings.json"
56
- elif scope == "local":
57
- return Path(working_directory) / ".letta" / "settings.local.json"
58
- else:
59
- raise ValueError(f"Unknown scope: {scope}")
60
-
61
-
62
- def load_settings(path: Path) -> dict:
63
- if path.exists():
64
- try:
65
- with open(path) as f:
66
- return json.load(f)
67
- except json.JSONDecodeError:
68
- print(f"Warning: Could not parse {path}, starting fresh", file=sys.stderr)
69
- return {}
70
- return {}
71
-
72
-
73
- def save_settings(path: Path, settings: dict) -> None:
74
- path.parent.mkdir(parents=True, exist_ok=True)
75
- with open(path, "w") as f:
76
- json.dump(settings, f, indent=2)
77
- print(f"Saved to {path}")
78
-
79
-
80
- def build_hook_config(args) -> dict:
81
- """Build the individual hook config from args."""
82
- hook: dict = {"type": args.type}
83
-
84
- if args.type == "command":
85
- if not args.command:
86
- raise ValueError("--command is required for type=command")
87
- hook["command"] = args.command
88
- elif args.type == "prompt":
89
- if not args.prompt:
90
- raise ValueError("--prompt is required for type=prompt")
91
- if args.event not in PROMPT_SUPPORTED:
92
- raise ValueError(
93
- f"Event {args.event!r} does not support prompt hooks. "
94
- f"Supported: {sorted(PROMPT_SUPPORTED)}"
95
- )
96
- hook["prompt"] = args.prompt
97
- if args.model:
98
- hook["model"] = args.model
99
-
100
- if args.timeout is not None:
101
- hook["timeout"] = args.timeout
102
-
103
- return hook
104
-
105
-
106
- def add_hook(settings: dict, args) -> None:
107
- """Add a hook entry to the settings dict."""
108
- if "hooks" not in settings:
109
- settings["hooks"] = {}
110
-
111
- hooks_config = settings["hooks"]
112
- event = args.event
113
-
114
- if event not in hooks_config:
115
- hooks_config[event] = []
116
-
117
- hook = build_hook_config(args)
118
-
119
- if event in TOOL_EVENTS:
120
- # Tool events: need a matcher
121
- matcher = args.matcher or "*"
122
- # Find existing matcher group or create new one
123
- entry = next(
124
- (e for e in hooks_config[event] if e.get("matcher") == matcher), None
125
- )
126
- if entry is None:
127
- entry = {"matcher": matcher, "hooks": []}
128
- hooks_config[event].append(entry)
129
- entry["hooks"].append(hook)
130
- else:
131
- # Simple events: no matcher, just hooks
132
- if hooks_config[event]:
133
- # Append to existing group
134
- hooks_config[event][0].setdefault("hooks", []).append(hook)
135
- else:
136
- hooks_config[event].append({"hooks": [hook]})
137
-
138
-
139
- def ensure_local_gitignored(working_directory: str) -> None:
140
- gitignore_path = Path(working_directory) / ".gitignore"
141
- pattern = ".letta/settings.local.json"
142
- try:
143
- content = gitignore_path.read_text() if gitignore_path.exists() else ""
144
- if pattern not in content:
145
- with open(gitignore_path, "a") as f:
146
- if content and not content.endswith("\n"):
147
- f.write("\n")
148
- f.write(f"{pattern}\n")
149
- print(f"Added {pattern} to .gitignore")
150
- except Exception as e:
151
- print(f"Warning: Could not update .gitignore: {e}", file=sys.stderr)
152
-
153
-
154
- def main():
155
- parser = argparse.ArgumentParser(
156
- description="Add a hook to Letta Code settings",
157
- formatter_class=argparse.RawDescriptionHelpFormatter,
158
- epilog=__doc__,
159
- )
160
- parser.add_argument(
161
- "--event",
162
- required=True,
163
- choices=sorted(ALL_EVENTS),
164
- help="Hook event name",
165
- )
166
- parser.add_argument(
167
- "--matcher",
168
- help="Tool matcher pattern (for tool events). Examples: 'Bash', 'Edit|Write', '*'",
169
- )
170
- parser.add_argument(
171
- "--type",
172
- required=True,
173
- choices=["command", "prompt"],
174
- help="Hook type",
175
- )
176
- parser.add_argument("--command", help="Shell command (for type=command)")
177
- parser.add_argument(
178
- "--prompt",
179
- help="LLM prompt text (for type=prompt). Use $ARGUMENTS for hook input JSON.",
180
- )
181
- parser.add_argument("--model", help="LLM model (for type=prompt)")
182
- parser.add_argument(
183
- "--timeout", type=int, help="Timeout in milliseconds (default: 60000/30000)"
184
- )
185
- parser.add_argument(
186
- "--scope",
187
- required=True,
188
- choices=["user", "project", "local"],
189
- help="Where to save the hook",
190
- )
191
- parser.add_argument(
192
- "--cwd",
193
- default=os.getcwd(),
194
- help="Working directory for project/local scope (default: cwd)",
195
- )
196
-
197
- args = parser.parse_args()
198
-
199
- # Validation
200
- if args.event in TOOL_EVENTS and not args.matcher:
201
- print(
202
- f"Warning: {args.event} is a tool event; using matcher='*' (match all tools)",
203
- file=sys.stderr,
204
- )
205
-
206
- settings_path = get_settings_path(args.scope, args.cwd)
207
- settings = load_settings(settings_path)
208
-
209
- try:
210
- add_hook(settings, args)
211
- except ValueError as e:
212
- print(f"Error: {e}", file=sys.stderr)
213
- sys.exit(1)
214
-
215
- save_settings(settings_path, settings)
216
- print(f"Added {args.type} hook on {args.event}")
217
-
218
- if args.scope == "local":
219
- ensure_local_gitignored(args.cwd)
220
-
221
-
222
- if __name__ == "__main__":
223
- main()
@@ -1,136 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Add a permission rule to Letta Code settings.
4
-
5
- Usage:
6
- python3 add_permission.py --rule "Bash(npm run:*)" --type allow --scope user
7
- python3 add_permission.py --rule "Read(src/**)" --type allow --scope project
8
- python3 add_permission.py --rule "Bash(git push:*)" --type alwaysAsk --scope user
9
- """
10
-
11
- import argparse
12
- import json
13
- import os
14
- import sys
15
- from pathlib import Path
16
-
17
-
18
- def get_settings_path(scope: str, working_directory: str) -> Path:
19
- """Get the settings file path for a given scope."""
20
- if scope == "user":
21
- return Path.home() / ".letta" / "settings.json"
22
- elif scope == "project":
23
- return Path(working_directory) / ".letta" / "settings.json"
24
- elif scope == "local":
25
- return Path(working_directory) / ".letta" / "settings.local.json"
26
- else:
27
- raise ValueError(f"Unknown scope: {scope}")
28
-
29
-
30
- def load_settings(path: Path) -> dict:
31
- """Load settings from a JSON file, or return empty dict if not found."""
32
- if path.exists():
33
- try:
34
- with open(path) as f:
35
- return json.load(f)
36
- except json.JSONDecodeError:
37
- print(f"Warning: Could not parse {path}, starting fresh", file=sys.stderr)
38
- return {}
39
- return {}
40
-
41
-
42
- def save_settings(path: Path, settings: dict) -> None:
43
- """Save settings to a JSON file, creating parent directories if needed."""
44
- path.parent.mkdir(parents=True, exist_ok=True)
45
- with open(path, "w") as f:
46
- json.dump(settings, f, indent=2)
47
- print(f"Saved to {path}")
48
-
49
-
50
- def add_rule(settings: dict, rule: str, rule_type: str) -> bool:
51
- """
52
- Add a permission rule to settings.
53
-
54
- Returns True if the rule was added, False if it already exists.
55
- """
56
- if "permissions" not in settings:
57
- settings["permissions"] = {}
58
-
59
- if rule_type not in settings["permissions"]:
60
- settings["permissions"][rule_type] = []
61
-
62
- rules = settings["permissions"][rule_type]
63
-
64
- if rule in rules:
65
- return False
66
-
67
- rules.append(rule)
68
- return True
69
-
70
-
71
- def ensure_local_gitignored(working_directory: str) -> None:
72
- """Ensure .letta/settings.local.json is in .gitignore."""
73
- gitignore_path = Path(working_directory) / ".gitignore"
74
- pattern = ".letta/settings.local.json"
75
-
76
- try:
77
- content = ""
78
- if gitignore_path.exists():
79
- content = gitignore_path.read_text()
80
-
81
- if pattern not in content:
82
- with open(gitignore_path, "a") as f:
83
- if content and not content.endswith("\n"):
84
- f.write("\n")
85
- f.write(f"{pattern}\n")
86
- print(f"Added {pattern} to .gitignore")
87
- except Exception as e:
88
- print(f"Warning: Could not update .gitignore: {e}", file=sys.stderr)
89
-
90
-
91
- def main():
92
- parser = argparse.ArgumentParser(
93
- description="Add a permission rule to Letta Code settings"
94
- )
95
- parser.add_argument(
96
- "--rule",
97
- required=True,
98
- help='Permission rule pattern, e.g., "Bash(npm run:*)" or "Read(src/**)"',
99
- )
100
- parser.add_argument(
101
- "--type",
102
- required=True,
103
- choices=["allow", "deny", "ask", "alwaysAsk"],
104
- help="Type of permission rule",
105
- )
106
- parser.add_argument(
107
- "--scope",
108
- required=True,
109
- choices=["user", "project", "local"],
110
- help="Where to save the rule",
111
- )
112
- parser.add_argument(
113
- "--cwd",
114
- default=os.getcwd(),
115
- help="Working directory for project/local scope (default: current directory)",
116
- )
117
-
118
- args = parser.parse_args()
119
-
120
- settings_path = get_settings_path(args.scope, args.cwd)
121
- settings = load_settings(settings_path)
122
-
123
- if add_rule(settings, args.rule, args.type):
124
- save_settings(settings_path, settings)
125
- print(f"Added {args.type} rule: {args.rule}")
126
-
127
- # Ensure local settings are gitignored
128
- if args.scope == "local":
129
- ensure_local_gitignored(args.cwd)
130
- else:
131
- print(f"Rule already exists: {args.rule}")
132
- sys.exit(0)
133
-
134
-
135
- if __name__ == "__main__":
136
- main()
@@ -1,212 +0,0 @@
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
- rule_types = ["allow", "deny", "ask", "alwaysAsk"]
44
- rules: dict[str, list[tuple[str, str]]] = {rule_type: [] for rule_type in rule_types}
45
- for scope, settings in all_settings:
46
- perms = settings.get("permissions", {})
47
- for rule_type in rule_types:
48
- for rule in perms.get(rule_type, []):
49
- rules[rule_type].append((rule, scope))
50
-
51
- if as_json:
52
- return {
53
- t: [{"rule": r, "scope": s} for r, s in rules[t]] for t in rules if rules[t]
54
- }
55
-
56
- total = sum(len(v) for v in rules.values())
57
- print("=" * 60)
58
- print(f"PERMISSIONS ({total} rules)")
59
- print("=" * 60)
60
- if total == 0:
61
- print(" (none)")
62
- else:
63
- for rule_type in rule_types:
64
- if rules[rule_type]:
65
- print(f"\n {rule_type.upper()}:")
66
- for rule, scope in rules[rule_type]:
67
- print(f" [{scope:7}] {rule}")
68
- print()
69
- return None
70
-
71
-
72
- def format_hooks(all_settings: list[tuple[str, dict]], as_json: bool) -> dict | None:
73
- """Collect hooks from all scopes with sources."""
74
- collected: dict[str, list[tuple[str, dict]]] = {}
75
- for scope, settings in all_settings:
76
- hooks = settings.get("hooks", {})
77
- if not isinstance(hooks, dict):
78
- continue
79
- for event, entries in hooks.items():
80
- if event == "disabled":
81
- continue
82
- if not isinstance(entries, list):
83
- continue
84
- collected.setdefault(event, []).extend((scope, e) for e in entries)
85
-
86
- if as_json:
87
- return {
88
- event: [
89
- {"scope": scope, **entry}
90
- for scope, entry in entries
91
- ]
92
- for event, entries in collected.items()
93
- }
94
-
95
- total_groups = sum(len(v) for v in collected.values())
96
- print("=" * 60)
97
- print(f"HOOKS ({len(collected)} events, {total_groups} groups)")
98
- print("=" * 60)
99
- if not collected:
100
- print(" (none)")
101
- else:
102
- for event, entries in sorted(collected.items()):
103
- print(f"\n {event}:")
104
- for scope, entry in entries:
105
- matcher = entry.get("matcher", "(no matcher)")
106
- hook_list = entry.get("hooks", [])
107
- for h in hook_list:
108
- htype = h.get("type", "?")
109
- detail = h.get("command") or h.get("prompt", "")
110
- detail = (
111
- (detail[:60] + "...") if len(detail) > 60 else detail
112
- )
113
- print(f" [{scope:7}] matcher={matcher:15} {htype}: {detail}")
114
- print()
115
- return None
116
-
117
-
118
- def format_agents(all_settings: list[tuple[str, dict]], as_json: bool) -> dict | None:
119
- """Collect per-agent settings (only from user settings.json)."""
120
- agents = []
121
- for scope, settings in all_settings:
122
- for a in settings.get("agents", []):
123
- agents.append({"scope": scope, **a})
124
-
125
- if as_json:
126
- return agents
127
-
128
- print("=" * 60)
129
- print(f"PER-AGENT SETTINGS ({len(agents)} entries)")
130
- print("=" * 60)
131
- if not agents:
132
- print(" (none)")
133
- else:
134
- for a in agents:
135
- scope = a.get("scope", "?")
136
- aid = a.get("agentId", "?")
137
- print(f"\n [{scope:7}] {aid}")
138
- for k in ("pinned", "memfs", "toolset", "systemPromptPreset", "baseUrl"):
139
- if k in a:
140
- val = a[k]
141
- if isinstance(val, dict):
142
- val = json.dumps(val)
143
- print(f" {k}: {val}")
144
- print()
145
- return None
146
-
147
-
148
- def format_settings_files(working_directory: str) -> None:
149
- print("=" * 60)
150
- print("SETTINGS FILES")
151
- print("=" * 60)
152
- for scope, path in get_settings_paths(working_directory):
153
- exists = "✓" if path.exists() else "✗"
154
- print(f" {exists} [{scope:7}] {path}")
155
- print()
156
-
157
-
158
- def main():
159
- parser = argparse.ArgumentParser(
160
- description="Show Letta Code harness configuration (permissions, hooks, agents)"
161
- )
162
- parser.add_argument(
163
- "--cwd",
164
- default=os.getcwd(),
165
- help="Working directory for project/local scope (default: cwd)",
166
- )
167
- parser.add_argument("--json", action="store_true", help="Output as JSON")
168
- parser.add_argument(
169
- "--section",
170
- choices=["permissions", "hooks", "agents", "all"],
171
- default="all",
172
- help="Which section to show (default: all)",
173
- )
174
-
175
- args = parser.parse_args()
176
-
177
- all_settings = [
178
- (scope, load_settings(path))
179
- for scope, path in get_settings_paths(args.cwd)
180
- ]
181
-
182
- if args.json:
183
- output: dict = {}
184
- if args.section in ("permissions", "all"):
185
- perms = format_permissions(all_settings, as_json=True)
186
- if perms:
187
- output["permissions"] = perms
188
- if args.section in ("hooks", "all"):
189
- hooks = format_hooks(all_settings, as_json=True)
190
- if hooks:
191
- output["hooks"] = hooks
192
- if args.section in ("agents", "all"):
193
- agents = format_agents(all_settings, as_json=True)
194
- if agents:
195
- output["agents"] = agents
196
- print(json.dumps(output, indent=2))
197
- else:
198
- print(f"\nLetta Code Harness Configuration")
199
- print(f"Working directory: {args.cwd}\n")
200
- if args.section == "all":
201
- format_settings_files(args.cwd)
202
- if args.section in ("permissions", "all"):
203
- format_permissions(all_settings, as_json=False)
204
- if args.section in ("hooks", "all"):
205
- format_hooks(all_settings, as_json=False)
206
- if args.section in ("agents", "all"):
207
- format_agents(all_settings, as_json=False)
208
- print("Precedence (highest to lowest): local > project > user\n")
209
-
210
-
211
- if __name__ == "__main__":
212
- main()