@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
@@ -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()