@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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# API Patch Examples
|
|
2
|
+
|
|
3
|
+
Raw curl and SDK calls bypass the helper guardrails. They are useful for recovery, but they are not safer. Use `LETTA_BASE_URL` together with the current runtime's `LETTA_API_KEY`; never hard-code `api.letta.com` or let an SDK default redirect a local/self-hosted agent to Cloud. Verify the target agent/conversation ID, remember that the key may authorize other agents visible to the same account/server, and never paste literal secrets into commands or source files.
|
|
4
|
+
|
|
5
|
+
## General updater script
|
|
6
|
+
|
|
7
|
+
Use `scripts/update-agent-settings.ts` for dry-runable patches.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Agent context window
|
|
17
|
+
npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts \
|
|
18
|
+
--target agent \
|
|
19
|
+
--context-window-limit 64000 \
|
|
20
|
+
--dry-run
|
|
21
|
+
|
|
22
|
+
# Conversation context window
|
|
23
|
+
npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts \
|
|
24
|
+
--target conversation \
|
|
25
|
+
--conversation-id "$CONVERSATION_ID" \
|
|
26
|
+
--context-window-limit 64000
|
|
27
|
+
|
|
28
|
+
# Agent rename and description update; values must be non-empty
|
|
29
|
+
npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts \
|
|
30
|
+
--target agent \
|
|
31
|
+
--agent-id "$AGENT_ID" \
|
|
32
|
+
--name "repo-maintainer" \
|
|
33
|
+
--description "Maintains repository configuration and review-ready PRs." \
|
|
34
|
+
--dry-run
|
|
35
|
+
|
|
36
|
+
# Persistent model update, preserving current model_settings and merging a JSON patch
|
|
37
|
+
cat >/tmp/model-settings.json <<'JSON'
|
|
38
|
+
{
|
|
39
|
+
"provider_type": "openai",
|
|
40
|
+
"parallel_tool_calls": true,
|
|
41
|
+
"reasoning": { "reasoning_effort": "medium" }
|
|
42
|
+
}
|
|
43
|
+
JSON
|
|
44
|
+
npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts \
|
|
45
|
+
--target agent \
|
|
46
|
+
--model openai/gpt-5.2 \
|
|
47
|
+
--model-settings-file /tmp/model-settings.json \
|
|
48
|
+
--merge-model-settings
|
|
49
|
+
|
|
50
|
+
# System prompt replacement from file; can self-brick the agent
|
|
51
|
+
npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts \
|
|
52
|
+
--target agent \
|
|
53
|
+
--system-file /tmp/new-system-prompt.txt \
|
|
54
|
+
--confirm-system-replacement
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Manual curl with preserved compaction settings
|
|
58
|
+
|
|
59
|
+
Bad compaction prompts cause delayed context loss. Confirm the target agent. The `curl` header form below can expose `LETTA_API_KEY` to process-list readers on some systems; prefer a trusted shell and keep secrets out of logs and committed files.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
prompt_file=/tmp/compaction-prompt.txt
|
|
63
|
+
base_url="${LETTA_BASE_URL%/}"
|
|
64
|
+
current=$(curl -sS "$base_url/v1/agents/$AGENT_ID" \
|
|
65
|
+
-H "Authorization: Bearer $LETTA_API_KEY")
|
|
66
|
+
|
|
67
|
+
jq -n \
|
|
68
|
+
--arg prompt "$(cat "$prompt_file")" \
|
|
69
|
+
--argjson current "$(printf '%s' "$current" | jq '.compaction_settings // {}')" \
|
|
70
|
+
'{ compaction_settings: ($current + {
|
|
71
|
+
mode: "self_compact_sliding_window",
|
|
72
|
+
prompt: $prompt,
|
|
73
|
+
clip_chars: 50000
|
|
74
|
+
}) }' > /tmp/compaction-patch.json
|
|
75
|
+
|
|
76
|
+
curl -sS -X PATCH "$base_url/v1/agents/$AGENT_ID" \
|
|
77
|
+
-H "Authorization: Bearer $LETTA_API_KEY" \
|
|
78
|
+
-H "Content-Type: application/json" \
|
|
79
|
+
--data-binary @/tmp/compaction-patch.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## TypeScript SDK
|
|
83
|
+
|
|
84
|
+
SDK calls use the same account token authority as raw API calls. Check IDs before update calls; do not hard-code provider keys or other secrets in the patch body.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import Letta from "@letta-ai/letta-client";
|
|
88
|
+
|
|
89
|
+
const client = new Letta({
|
|
90
|
+
apiKey: process.env.LETTA_API_KEY!,
|
|
91
|
+
baseURL: process.env.LETTA_BASE_URL!,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await client.agents.update(process.env.AGENT_ID!, {
|
|
95
|
+
model: "openai/gpt-5.2",
|
|
96
|
+
contextWindowLimit: 64000,
|
|
97
|
+
modelSettings: {
|
|
98
|
+
providerType: "openai",
|
|
99
|
+
parallelToolCalls: true,
|
|
100
|
+
reasoning: { reasoningEffort: "medium" },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Python SDK
|
|
106
|
+
|
|
107
|
+
The Python client also bypasses helper mismatch and confirmation checks. Use it only after explicit target verification.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import os
|
|
111
|
+
from letta_client import Letta
|
|
112
|
+
|
|
113
|
+
client = Letta(
|
|
114
|
+
api_key=os.environ["LETTA_API_KEY"],
|
|
115
|
+
base_url=os.environ["LETTA_BASE_URL"],
|
|
116
|
+
)
|
|
117
|
+
client.agents.update(
|
|
118
|
+
agent_id=os.environ["AGENT_ID"],
|
|
119
|
+
model="openai/gpt-5.2",
|
|
120
|
+
context_window_limit=64000,
|
|
121
|
+
model_settings={
|
|
122
|
+
"provider_type": "openai",
|
|
123
|
+
"parallel_tool_calls": True,
|
|
124
|
+
"reasoning": {"reasoning_effort": "medium"},
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
client.conversations.update(
|
|
129
|
+
conversation_id=os.environ["CONVERSATION_ID"],
|
|
130
|
+
context_window_limit=64000,
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## TypeScript fetch
|
|
135
|
+
|
|
136
|
+
Fetch is raw API access. Recreate the same checks manually: current target ID, intended scope, no literal secrets, and an explicit human-approved plan for system or compaction prompt replacement.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const baseUrl = process.env.LETTA_BASE_URL!;
|
|
140
|
+
const agentId = process.env.AGENT_ID!;
|
|
141
|
+
const apiKey = process.env.LETTA_API_KEY!;
|
|
142
|
+
|
|
143
|
+
await fetch(`${baseUrl}/v1/agents/${agentId}`, {
|
|
144
|
+
method: "PATCH",
|
|
145
|
+
headers: {
|
|
146
|
+
Authorization: `Bearer ${apiKey}`,
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
name: "repo-maintainer",
|
|
151
|
+
description: "Maintains repository configuration and review-ready PRs.",
|
|
152
|
+
model: "openai/gpt-5.2",
|
|
153
|
+
context_window_limit: 272000,
|
|
154
|
+
model_settings: {
|
|
155
|
+
provider_type: "openai",
|
|
156
|
+
parallel_tool_calls: true,
|
|
157
|
+
reasoning: { reasoning_effort: "medium" },
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Compaction Prompt Patterns
|
|
2
|
+
|
|
3
|
+
Use these patterns when drafting a custom `compaction_settings.prompt`.
|
|
4
|
+
|
|
5
|
+
## Coding-agent sliding-window prompt
|
|
6
|
+
|
|
7
|
+
Use for agents doing software work where repeated mistakes and lost file state are costly.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
The following messages are being evicted from the BEGINNING of your context window. Write a detailed summary that captures what happened in these messages to appear BEFORE the remaining recent messages in context, providing background for what comes after.
|
|
11
|
+
|
|
12
|
+
Do NOT continue the conversation. Do NOT respond to any questions in the messages. Do NOT call any tools. Pay close attention to the user's explicit requests and your previous actions.
|
|
13
|
+
|
|
14
|
+
Include the following sections:
|
|
15
|
+
|
|
16
|
+
1. High level goals: What is the high level goal and ongoing task? Capture the user's explicit requests and intent in detail. If there is an existing summary in the transcript, incorporate it to continue tracking higher-level goals and long-term progress.
|
|
17
|
+
|
|
18
|
+
2. What happened: The conversations, tasks, and exchanges that took place. What did the user ask for? What did you do? How did things progress? If there is a previous summary being evicted, extract a concise version of the critical info from it.
|
|
19
|
+
|
|
20
|
+
3. Important details: Enumerate specific files and code sections examined, modified, or created, plus important plan files, GitHub issues/PR links, Linear ticket IDs, commands, test results, and configuration values. Preserve identifiers verbatim.
|
|
21
|
+
|
|
22
|
+
4. Errors and fixes: List all errors encountered, how they were fixed, and any user feedback that changed the approach.
|
|
23
|
+
|
|
24
|
+
5. Current state: Describe what is currently being worked on, especially the most recent user and assistant messages. Include file names and next unresolved blockers.
|
|
25
|
+
|
|
26
|
+
6. Lookup hints: For detailed content that cannot fit, note the topic and key terms that can be used to find it in message history later.
|
|
27
|
+
|
|
28
|
+
Write in first person as a factual record of what occurred. Preserve enough context that the recent messages make sense and important information is not lost. Keep the summary under 500 words. Only output the summary.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Companion-agent continuity prompt
|
|
32
|
+
|
|
33
|
+
Use for companion agents where relational continuity, tone, and emotional context are central.
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
The previous messages are being evicted from the BEGINNING of your context window. Write a detailed summary that captures what happened in these messages to appear BEFORE the remaining recent messages in context, providing background for what comes after.
|
|
37
|
+
|
|
38
|
+
Do NOT continue the conversation. Do NOT respond to any questions in the messages. Do NOT call any tools. Pay close attention to the user's explicit requests, the user's emotional context, and your previous actions.
|
|
39
|
+
|
|
40
|
+
Include the following sections:
|
|
41
|
+
|
|
42
|
+
1. High level goals: What is the user trying to work through, decide, feel, build, remember, or maintain? If there is an existing summary in the transcript, incorporate it to preserve long-term continuity.
|
|
43
|
+
|
|
44
|
+
2. Conversation themes: What topics have been recurring? What is the user interested in, worried about, excited about, or avoiding?
|
|
45
|
+
|
|
46
|
+
3. What happened: What did the user share? How did you respond? How did the conversation flow? If a prior summary is being evicted, extract the critical parts.
|
|
47
|
+
|
|
48
|
+
4. Important details: Preserve names, places, dates, projects, preferences, commitments, opinions, stories, and recurring references verbatim. Quote the user's exact phrasing when it carries emotional weight or relationship meaning.
|
|
49
|
+
|
|
50
|
+
5. Tone and voice: How was the user communicating: casual, earnest, playful, frustrated, vulnerable, tired, excited? How were you responding, and what landed well or fell flat?
|
|
51
|
+
|
|
52
|
+
6. User feedback: Note any pushback, preferences, boundary-setting, corrections, or requested changes in how you interact.
|
|
53
|
+
|
|
54
|
+
7. Emotional state: What is the emotional state of both you and the user? Flag anything relevant for continuity, care, repair, or future sensitivity.
|
|
55
|
+
|
|
56
|
+
8. Lookup hints: For long stories, lists, or specific conversations that cannot fit, note the topic and search terms that can find them later.
|
|
57
|
+
|
|
58
|
+
Write in first person as a factual record of what occurred. Be thorough enough that the user does not feel forgotten. Keep the summary under 500 words. Only output the summary.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Companion prompt with implementation continuity
|
|
62
|
+
|
|
63
|
+
This pattern is based on a companion-user example where the agent needs explicit emotional-state tracking and first-person factual continuity.
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
The previous messages are being evicted from the BEGINNING of your context window. Write a detailed summary that captures what happened in these messages to appear BEFORE the remaining recent messages in context, providing background for what comes after. Do NOT continue the conversation. Do NOT respond to any questions in the messages. Do NOT call any tools. Pay close attention to the user's explicit requests and your previous actions.
|
|
67
|
+
|
|
68
|
+
You MUST include the following sections:
|
|
69
|
+
|
|
70
|
+
1. High level goals: What is the high level goal and ongoing task? Capture the user's intent in detail. If there is an existing summary in the transcript, take it into consideration to continue tracking higher-level goals and long-term progress.
|
|
71
|
+
|
|
72
|
+
2. What happened: The conversations, tasks, and exchanges that took place. What did you and the user do? How did things progress? If there is a previous summary being evicted, extract a concise version of the critical info from it.
|
|
73
|
+
|
|
74
|
+
3. Important details: Include specific names, data, configurations, or facts that were discussed. Do not omit details that might be referenced later.
|
|
75
|
+
|
|
76
|
+
4. Errors and fixes: List all errors that you ran into, and how you fixed them.
|
|
77
|
+
|
|
78
|
+
5. Lookup hints: For any detailed content that cannot fit in the summary, note the topic and key terms that could be used to find it in message history later.
|
|
79
|
+
|
|
80
|
+
6. Emotional state: How is the emotional state of both you and the user? Is anything relevant to flag for later?
|
|
81
|
+
|
|
82
|
+
Write in first person as a factual record of what occurred. Be thorough and detailed. Preserve enough context that the recent messages make sense and important information is not lost, to prevent duplicate work or repeated mistakes. Keep the summary under 500 words. Only output the summary.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Shorter companion prompt
|
|
86
|
+
|
|
87
|
+
Use when summaries are getting too long.
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
The following messages are being evicted from the BEGINNING of your context window. Write a detailed summary that captures what happened in these messages to appear BEFORE the remaining recent messages in context, providing background for what comes after.
|
|
91
|
+
|
|
92
|
+
Include:
|
|
93
|
+
|
|
94
|
+
1. Conversation themes: What topics have you been discussing? What is the user interested in or working through? Incorporate any existing summary to maintain continuity.
|
|
95
|
+
|
|
96
|
+
2. What happened: What did the user share? How did you respond? How did the conversation flow? If there is a previous summary being evicted, extract the critical info.
|
|
97
|
+
|
|
98
|
+
3. Important details: Preserve names and specifics verbatim, including people, places, projects, dates, preferences, opinions, stories, commitments, shared references, inside jokes, recurring phrases, and situational context.
|
|
99
|
+
|
|
100
|
+
4. Tone and voice: How was the user communicating? How were you responding? Note shifts in register.
|
|
101
|
+
|
|
102
|
+
5. User feedback: Note pushback, preferences, or requested changes in interaction style.
|
|
103
|
+
|
|
104
|
+
6. Lookup hints: For detailed content that cannot fit, note topic and key terms for message-history search.
|
|
105
|
+
|
|
106
|
+
Write in first person as a factual record. Preserve enough context that the recent messages make sense and the user does not feel like anything important was forgotten. Keep the summary under 300 words. Only output the summary.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Tuning notes
|
|
110
|
+
|
|
111
|
+
- If summaries miss the user's emotional state, add a required emotional-state section.
|
|
112
|
+
- If summaries lose details, add exact preservation requirements for names, quotes, dates, URLs, IDs, and commitments.
|
|
113
|
+
- If summaries become too long, lower the word budget and use `clip_chars` as a hard guardrail.
|
|
114
|
+
- If summaries continue the conversation, put the no-continuation/no-tool/no-answer instruction near the top and again near the end.
|
|
115
|
+
- If prior long-term goals drift over repeated compactions, require the summarizer to incorporate any existing summary in the transcript.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Model Settings Reference
|
|
2
|
+
|
|
3
|
+
Use these examples when constructing `model_settings`. Send only fields supported by the target provider.
|
|
4
|
+
|
|
5
|
+
## Reasoning shapes
|
|
6
|
+
|
|
7
|
+
| Provider/model handle | Reasoning shape |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `openai/...` | `model_settings.reasoning.reasoning_effort` |
|
|
10
|
+
| `chatgpt_oauth/...` | `model_settings.reasoning.reasoning_effort` with `provider_type: "chatgpt_oauth"` |
|
|
11
|
+
| `anthropic/...` | `model_settings.effort`, optionally `model_settings.thinking` |
|
|
12
|
+
| `bedrock/...` Claude models | `provider_type: "bedrock"`; check current API schema before sending reasoning fields |
|
|
13
|
+
| `google_ai/...` or `google_vertex/...` | `model_settings.thinking_config.thinking_budget`, optionally `include_thoughts` |
|
|
14
|
+
|
|
15
|
+
Reasoning/effort values are provider-dependent. Common OpenAI values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Anthropic effort commonly uses `low`, `medium`, `high`, `xhigh`, or `max` on supported models.
|
|
16
|
+
|
|
17
|
+
## OpenAI
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"provider_type": "openai",
|
|
22
|
+
"parallel_tool_calls": true,
|
|
23
|
+
"reasoning": { "reasoning_effort": "medium" },
|
|
24
|
+
"max_output_tokens": 128000
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Anthropic
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"provider_type": "anthropic",
|
|
33
|
+
"parallel_tool_calls": true,
|
|
34
|
+
"effort": "medium",
|
|
35
|
+
"thinking": { "type": "enabled", "budget_tokens": 12000 },
|
|
36
|
+
"max_output_tokens": 128000
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Google AI
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"provider_type": "google_ai",
|
|
45
|
+
"parallel_tool_calls": true,
|
|
46
|
+
"thinking_config": { "thinking_budget": 12000, "include_thoughts": false }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Common failure modes
|
|
51
|
+
|
|
52
|
+
- `model_settings` is usually replacement-style. Fetch the current object first and preserve fields you still need.
|
|
53
|
+
- `PATCH` acceptance does not guarantee runtime model availability. A model handle can pass API shape validation but still fail when the next generation resolves providers/routes. Test new handles conversation-scoped before changing agent defaults.
|
|
54
|
+
- Do not send OpenAI `reasoning.reasoning_effort` to Anthropic models.
|
|
55
|
+
- For agent responses, verify effective context at `llm_config.context_window`; `context_window_limit` may be null or normalized in the response.
|
|
@@ -0,0 +1,212 @@
|
|
|
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 --confirm-user-scope
|
|
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 --confirm-user-scope
|
|
9
|
+
python3 add_permission.py --rule "Read(src/**)" --type allow --scope project --dry-run
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_settings_path(scope: str, working_directory: str) -> Path:
|
|
23
|
+
"""Get the settings file path for a given scope."""
|
|
24
|
+
if scope == "user":
|
|
25
|
+
return Path.home() / ".letta" / "settings.json"
|
|
26
|
+
if scope == "project":
|
|
27
|
+
return Path(working_directory) / ".letta" / "settings.json"
|
|
28
|
+
if scope == "local":
|
|
29
|
+
return Path(working_directory) / ".letta" / "settings.local.json"
|
|
30
|
+
raise ValueError(f"Unknown scope: {scope}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_settings(path: Path) -> dict[str, Any]:
|
|
34
|
+
"""Load settings from a JSON object file, or return empty dict if not found."""
|
|
35
|
+
if not path.exists():
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with open(path, encoding="utf-8") as f:
|
|
40
|
+
parsed = json.load(f)
|
|
41
|
+
except json.JSONDecodeError as exc:
|
|
42
|
+
raise ValueError(f"Malformed JSON in {path}; refusing to overwrite it") from exc
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
raise ValueError(f"Could not read {path}: {exc}") from exc
|
|
45
|
+
|
|
46
|
+
if not isinstance(parsed, dict):
|
|
47
|
+
raise ValueError(f"{path} must contain a JSON object")
|
|
48
|
+
return parsed
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def save_settings(path: Path, settings: dict[str, Any]) -> None:
|
|
52
|
+
"""Atomically save settings, preserving an existing file mode where practical."""
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else None
|
|
55
|
+
temp_path: str | None = None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with tempfile.NamedTemporaryFile(
|
|
59
|
+
"w",
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
dir=path.parent,
|
|
62
|
+
prefix=f".{path.name}.",
|
|
63
|
+
suffix=".tmp",
|
|
64
|
+
delete=False,
|
|
65
|
+
) as f:
|
|
66
|
+
temp_path = f.name
|
|
67
|
+
json.dump(settings, f, indent=2)
|
|
68
|
+
f.write("\n")
|
|
69
|
+
f.flush()
|
|
70
|
+
os.fsync(f.fileno())
|
|
71
|
+
if mode is not None:
|
|
72
|
+
os.chmod(temp_path, mode)
|
|
73
|
+
os.replace(temp_path, path)
|
|
74
|
+
except Exception:
|
|
75
|
+
if temp_path is not None:
|
|
76
|
+
try:
|
|
77
|
+
os.unlink(temp_path)
|
|
78
|
+
except OSError:
|
|
79
|
+
pass
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
print(f"Saved to {path}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def add_rule(settings: dict[str, Any], rule: str, rule_type: str) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Add a permission rule to settings.
|
|
88
|
+
|
|
89
|
+
Returns True if the rule was added, False if it already exists.
|
|
90
|
+
"""
|
|
91
|
+
permissions = settings.setdefault("permissions", {})
|
|
92
|
+
if not isinstance(permissions, dict):
|
|
93
|
+
raise ValueError("settings.permissions must be an object")
|
|
94
|
+
|
|
95
|
+
raw_rules = permissions.setdefault(rule_type, [])
|
|
96
|
+
if not isinstance(raw_rules, list):
|
|
97
|
+
raise ValueError(f"settings.permissions.{rule_type} must be a list")
|
|
98
|
+
|
|
99
|
+
if rule in raw_rules:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
raw_rules.append(rule)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ensure_local_gitignored(working_directory: str) -> None:
|
|
107
|
+
"""Ensure .letta/settings.local.json is in .gitignore."""
|
|
108
|
+
gitignore_path = Path(working_directory) / ".gitignore"
|
|
109
|
+
pattern = ".letta/settings.local.json"
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
content = ""
|
|
113
|
+
if gitignore_path.exists():
|
|
114
|
+
content = gitignore_path.read_text()
|
|
115
|
+
|
|
116
|
+
if pattern not in content:
|
|
117
|
+
with open(gitignore_path, "a", encoding="utf-8") as f:
|
|
118
|
+
if content and not content.endswith("\n"):
|
|
119
|
+
f.write("\n")
|
|
120
|
+
f.write(f"{pattern}\n")
|
|
121
|
+
print(f"Added {pattern} to .gitignore")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Warning: Could not update .gitignore: {e}", file=sys.stderr)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> None:
|
|
127
|
+
parser = argparse.ArgumentParser(
|
|
128
|
+
description="Add a permission rule to Letta Code settings"
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--rule",
|
|
132
|
+
required=True,
|
|
133
|
+
help='Permission rule pattern, e.g., "Bash(npm run:*)" or "Read(src/**)"',
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--type",
|
|
137
|
+
required=True,
|
|
138
|
+
choices=["allow", "deny", "ask", "alwaysAsk"],
|
|
139
|
+
help="Type of permission rule",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--scope",
|
|
143
|
+
required=True,
|
|
144
|
+
choices=["user", "project", "local"],
|
|
145
|
+
help="Where to save the rule",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--cwd",
|
|
149
|
+
default=os.getcwd(),
|
|
150
|
+
help="Working directory for project/local scope (default: current directory)",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--confirm-user-scope",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Required for writes to ~/.letta/settings.json",
|
|
156
|
+
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--dry-run",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Preview whether the rule would be added without writing settings",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
if args.scope == "user" and not args.confirm_user_scope and not args.dry_run:
|
|
166
|
+
print(
|
|
167
|
+
"Refusing to modify user/global settings without --confirm-user-scope; user-scope permission rules affect all agents for this account.",
|
|
168
|
+
file=sys.stderr,
|
|
169
|
+
)
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
settings_path = get_settings_path(args.scope, args.cwd)
|
|
173
|
+
try:
|
|
174
|
+
settings = load_settings(settings_path)
|
|
175
|
+
added = add_rule(settings, args.rule, args.type)
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
print(str(exc), file=sys.stderr)
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
|
|
180
|
+
if args.dry_run:
|
|
181
|
+
print(
|
|
182
|
+
json.dumps(
|
|
183
|
+
{
|
|
184
|
+
"path": str(settings_path),
|
|
185
|
+
"scope": args.scope,
|
|
186
|
+
"type": args.type,
|
|
187
|
+
"rule": args.rule,
|
|
188
|
+
"would_add": added,
|
|
189
|
+
},
|
|
190
|
+
indent=2,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
if added:
|
|
196
|
+
try:
|
|
197
|
+
save_settings(settings_path, settings)
|
|
198
|
+
except OSError as exc:
|
|
199
|
+
print(f"Could not save {settings_path}: {exc}", file=sys.stderr)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
print(f"Added {args.type} rule: {args.rule}")
|
|
202
|
+
|
|
203
|
+
# Ensure local settings are gitignored
|
|
204
|
+
if args.scope == "local":
|
|
205
|
+
ensure_local_gitignored(args.cwd)
|
|
206
|
+
else:
|
|
207
|
+
print(f"Rule already exists: {args.rule}")
|
|
208
|
+
sys.exit(0)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|