@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.
- package/dist/agent-presets.js +268 -36
- package/dist/agent-presets.js.map +2 -2
- package/dist/channels-slack.js +2 -11
- package/dist/channels-slack.js.map +3 -3
- package/dist/types/agent/model-catalog.d.ts +0 -38
- package/dist/types/agent/model-catalog.d.ts.map +1 -1
- package/dist/types/channels/slack/progress.d.ts.map +1 -1
- package/dist/types/channels/types.d.ts +1 -0
- package/dist/types/channels/types.d.ts.map +1 -1
- package/letta.js +32445 -31820
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +4 -4
- package/skills/creating-mods/references/events.md +4 -4
- package/skills/self-configuration/LICENSE +21 -0
- package/skills/self-configuration/SKILL.md +432 -0
- package/skills/self-configuration/references/api-patch-examples.md +161 -0
- package/skills/self-configuration/references/compaction-prompt-patterns.md +115 -0
- package/skills/self-configuration/references/model-settings.md +55 -0
- package/skills/self-configuration/scripts/add_permission.py +212 -0
- package/skills/self-configuration/scripts/show_config.py +370 -0
- package/skills/self-configuration/scripts/update-agent-settings.ts +384 -0
- package/skills/self-configuration/scripts/update-compaction-prompt.ts +227 -0
- package/skills/modifying-the-harness/SKILL.md +0 -263
- package/skills/modifying-the-harness/references/hooks.md +0 -261
- package/skills/modifying-the-harness/scripts/add_hook.py +0 -223
- package/skills/modifying-the-harness/scripts/add_permission.py +0 -136
- 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()
|