erk 0.4.5__py3-none-any.whl
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.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Pure functions for Claude Code settings management.
|
|
2
|
+
|
|
3
|
+
This module provides functions to read and modify Claude Code settings,
|
|
4
|
+
specifically for managing permissions in the repo's .claude/settings.json.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
# The permission pattern that allows Claude to run erk commands without prompting
|
|
16
|
+
ERK_PERMISSION = "Bash(erk:*)"
|
|
17
|
+
|
|
18
|
+
# Hook commands for erk integration
|
|
19
|
+
ERK_USER_PROMPT_HOOK_COMMAND = "ERK_HOOK_ID=user-prompt-hook erk exec user-prompt-hook"
|
|
20
|
+
ERK_EXIT_PLAN_HOOK_COMMAND = "ERK_HOOK_ID=exit-plan-mode-hook erk exec exit-plan-mode-hook"
|
|
21
|
+
|
|
22
|
+
# Statusline command - can be overridden via ERK_STATUSLINE_COMMAND env var for dev mode
|
|
23
|
+
ERK_STATUSLINE_COMMAND = "uvx erk-statusline"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_erk_statusline_command() -> str:
|
|
27
|
+
"""Get the statusline command, checking env var for dev mode override.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
ERK_STATUSLINE_COMMAND env var if set, otherwise "uvx erk-statusline".
|
|
31
|
+
"""
|
|
32
|
+
return os.environ.get("ERK_STATUSLINE_COMMAND", ERK_STATUSLINE_COMMAND)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class NoBackupCreated:
|
|
37
|
+
"""Sentinel indicating no backup was created (file didn't exist)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_repo_claude_settings_path(repo_root: Path) -> Path:
|
|
41
|
+
"""Return the path to the repo's Claude settings file.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
repo_root: Path to the repository root
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Path to {repo_root}/.claude/settings.json
|
|
48
|
+
"""
|
|
49
|
+
return repo_root / ".claude" / "settings.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def has_erk_permission(settings: dict) -> bool:
|
|
53
|
+
"""Check if erk permission is configured in Claude settings.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
settings: Parsed Claude settings dictionary
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if Bash(erk:*) permission exists in permissions.allow list
|
|
60
|
+
"""
|
|
61
|
+
permissions = settings.get("permissions", {})
|
|
62
|
+
allow_list = permissions.get("allow", [])
|
|
63
|
+
return ERK_PERMISSION in allow_list
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def has_user_prompt_hook(settings: Mapping[str, Any]) -> bool:
|
|
67
|
+
"""Check if erk UserPromptSubmit hook is configured.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
settings: Parsed Claude settings dictionary
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if the erk UserPromptSubmit hook is configured
|
|
74
|
+
"""
|
|
75
|
+
hooks = settings.get("hooks", {})
|
|
76
|
+
user_prompt_hooks = hooks.get("UserPromptSubmit", [])
|
|
77
|
+
for entry in user_prompt_hooks:
|
|
78
|
+
for hook in entry.get("hooks", []):
|
|
79
|
+
if hook.get("command") == ERK_USER_PROMPT_HOOK_COMMAND:
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def has_exit_plan_hook(settings: Mapping[str, Any]) -> bool:
|
|
85
|
+
"""Check if erk ExitPlanMode hook is configured.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
settings: Parsed Claude settings dictionary
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if the erk ExitPlanMode PreToolUse hook is configured
|
|
92
|
+
"""
|
|
93
|
+
hooks = settings.get("hooks", {})
|
|
94
|
+
pre_tool_hooks = hooks.get("PreToolUse", [])
|
|
95
|
+
for entry in pre_tool_hooks:
|
|
96
|
+
if entry.get("matcher") == "ExitPlanMode":
|
|
97
|
+
for hook in entry.get("hooks", []):
|
|
98
|
+
if hook.get("command") == ERK_EXIT_PLAN_HOOK_COMMAND:
|
|
99
|
+
return True
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def add_erk_hooks(settings: Mapping[str, Any]) -> dict[str, Any]:
|
|
104
|
+
"""Return a new settings dict with erk hooks added.
|
|
105
|
+
|
|
106
|
+
This is a pure function that doesn't modify the input.
|
|
107
|
+
Adds missing hooks while preserving existing settings.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
settings: Parsed Claude settings dictionary
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
New settings dict with erk hooks added
|
|
114
|
+
"""
|
|
115
|
+
# Deep copy to avoid mutating input
|
|
116
|
+
new_settings = json.loads(json.dumps(settings))
|
|
117
|
+
|
|
118
|
+
# Use defaultdict for cleaner hook list initialization
|
|
119
|
+
hooks: defaultdict[str, list] = defaultdict(list, new_settings.get("hooks", {}))
|
|
120
|
+
|
|
121
|
+
# Add UserPromptSubmit hook if missing
|
|
122
|
+
if not has_user_prompt_hook(settings):
|
|
123
|
+
hooks["UserPromptSubmit"].append(
|
|
124
|
+
{
|
|
125
|
+
"matcher": "*",
|
|
126
|
+
"hooks": [
|
|
127
|
+
{
|
|
128
|
+
"type": "command",
|
|
129
|
+
"command": ERK_USER_PROMPT_HOOK_COMMAND,
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Add PreToolUse hook for ExitPlanMode if missing
|
|
136
|
+
if not has_exit_plan_hook(settings):
|
|
137
|
+
hooks["PreToolUse"].append(
|
|
138
|
+
{
|
|
139
|
+
"matcher": "ExitPlanMode",
|
|
140
|
+
"hooks": [
|
|
141
|
+
{
|
|
142
|
+
"type": "command",
|
|
143
|
+
"command": ERK_EXIT_PLAN_HOOK_COMMAND,
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
new_settings["hooks"] = dict(hooks)
|
|
150
|
+
return new_settings
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def add_erk_permission(settings: dict) -> dict:
|
|
154
|
+
"""Return a new settings dict with erk permission added.
|
|
155
|
+
|
|
156
|
+
This is a pure function that doesn't modify the input.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
settings: Parsed Claude settings dictionary
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
New settings dict with Bash(erk:*) added to permissions.allow
|
|
163
|
+
"""
|
|
164
|
+
# Deep copy to avoid mutating input
|
|
165
|
+
new_settings = json.loads(json.dumps(settings))
|
|
166
|
+
|
|
167
|
+
# Ensure permissions.allow exists
|
|
168
|
+
if "permissions" not in new_settings:
|
|
169
|
+
new_settings["permissions"] = {}
|
|
170
|
+
if "allow" not in new_settings["permissions"]:
|
|
171
|
+
new_settings["permissions"]["allow"] = []
|
|
172
|
+
|
|
173
|
+
# Add permission if not present
|
|
174
|
+
if ERK_PERMISSION not in new_settings["permissions"]["allow"]:
|
|
175
|
+
new_settings["permissions"]["allow"].append(ERK_PERMISSION)
|
|
176
|
+
|
|
177
|
+
return new_settings
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def read_claude_settings(settings_path: Path) -> dict | None:
|
|
181
|
+
"""Read and parse Claude settings from disk.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
settings_path: Path to settings.json file
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Parsed settings dict, or None if file doesn't exist
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
json.JSONDecodeError: If file contains invalid JSON
|
|
191
|
+
OSError: If file cannot be read
|
|
192
|
+
"""
|
|
193
|
+
if not settings_path.exists():
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
content = settings_path.read_text(encoding="utf-8")
|
|
197
|
+
return json.loads(content)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def write_claude_settings(settings_path: Path, settings: dict) -> Path | NoBackupCreated:
|
|
201
|
+
"""Write Claude settings to disk.
|
|
202
|
+
|
|
203
|
+
Creates a backup of the existing file before writing (if it exists).
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
settings_path: Path to settings.json file
|
|
207
|
+
settings: Settings dict to write
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Path to backup file if created, NoBackupCreated sentinel otherwise.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
PermissionError: If unable to write to file
|
|
214
|
+
OSError: If unable to write to file
|
|
215
|
+
"""
|
|
216
|
+
# Create backup of existing file (if it exists)
|
|
217
|
+
backup_result: Path | NoBackupCreated
|
|
218
|
+
if settings_path.exists():
|
|
219
|
+
backup_path = settings_path.with_suffix(".json.bak")
|
|
220
|
+
backup_path.write_bytes(settings_path.read_bytes())
|
|
221
|
+
backup_result = backup_path
|
|
222
|
+
else:
|
|
223
|
+
backup_result = NoBackupCreated()
|
|
224
|
+
|
|
225
|
+
# Ensure parent directory exists
|
|
226
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
|
|
228
|
+
# Write with pretty formatting to match Claude's style
|
|
229
|
+
content = json.dumps(settings, indent=2)
|
|
230
|
+
settings_path.write_text(content, encoding="utf-8")
|
|
231
|
+
|
|
232
|
+
return backup_result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class StatuslineConfig:
|
|
237
|
+
"""Represents the current statusline configuration."""
|
|
238
|
+
|
|
239
|
+
type: str
|
|
240
|
+
command: str
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class StatuslineNotConfigured:
|
|
245
|
+
"""Sentinel indicating statusline is not configured."""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def has_statusline_configured(settings: dict) -> bool:
|
|
249
|
+
"""Check if statusLine is configured in Claude settings.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
settings: Parsed Claude settings dictionary
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if statusLine configuration exists
|
|
256
|
+
"""
|
|
257
|
+
return "statusLine" in settings
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_statusline_config(settings: dict) -> StatuslineConfig | StatuslineNotConfigured:
|
|
261
|
+
"""Get the current statusline configuration from settings.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
settings: Parsed Claude settings dictionary
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
StatuslineConfig if configured, StatuslineNotConfigured otherwise
|
|
268
|
+
"""
|
|
269
|
+
statusline = settings.get("statusLine")
|
|
270
|
+
if statusline is None:
|
|
271
|
+
return StatuslineNotConfigured()
|
|
272
|
+
|
|
273
|
+
statusline_type = statusline.get("type")
|
|
274
|
+
command = statusline.get("command")
|
|
275
|
+
|
|
276
|
+
if statusline_type is None or command is None:
|
|
277
|
+
return StatuslineNotConfigured()
|
|
278
|
+
|
|
279
|
+
return StatuslineConfig(type=statusline_type, command=command)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def has_erk_statusline(settings: dict) -> bool:
|
|
283
|
+
"""Check if erk-statusline is already configured.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
settings: Parsed Claude settings dictionary
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
True if statusLine is configured with a command containing "erk-statusline"
|
|
290
|
+
"""
|
|
291
|
+
config = get_statusline_config(settings)
|
|
292
|
+
if isinstance(config, StatuslineNotConfigured):
|
|
293
|
+
return False
|
|
294
|
+
# Accept any command containing "erk-statusline" (with or without uvx prefix)
|
|
295
|
+
return "erk-statusline" in config.command
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def add_erk_statusline(settings: dict) -> dict:
|
|
299
|
+
"""Return a new settings dict with erk-statusline configured.
|
|
300
|
+
|
|
301
|
+
This is a pure function that doesn't modify the input.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
settings: Parsed Claude settings dictionary
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
New settings dict with statusLine configured
|
|
308
|
+
"""
|
|
309
|
+
# Deep copy to avoid mutating input
|
|
310
|
+
new_settings = json.loads(json.dumps(settings))
|
|
311
|
+
|
|
312
|
+
new_settings["statusLine"] = {
|
|
313
|
+
"type": "command",
|
|
314
|
+
"command": get_erk_statusline_command(),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return new_settings
|