trigger-tree 1.14.0__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.
- trigger_tree/__init__.py +1 -0
- trigger_tree/_scripts/tt-codex-hook.py +120 -0
- trigger_tree/_scripts/tt-config.sh +32 -0
- trigger_tree/_scripts/tt-doctor.py +288 -0
- trigger_tree/_scripts/tt-log.py +656 -0
- trigger_tree/_scripts/tt-open.sh +164 -0
- trigger_tree/_scripts/tt-publish-badge.sh +26 -0
- trigger_tree/_scripts/tt-report.py +623 -0
- trigger_tree/_scripts/tt-setup.py +266 -0
- trigger_tree/_scripts/tt-shell-capture.sh +48 -0
- trigger_tree/_scripts/tt-stats.py +937 -0
- trigger_tree/_scripts/tt-statusline.py +154 -0
- trigger_tree/_scripts/tt-suggestions.py +128 -0
- trigger_tree/_scripts/tt-tips.py +107 -0
- trigger_tree/_scripts/tt-uninstall.py +60 -0
- trigger_tree/_scripts/tt-watch.py +1013 -0
- trigger_tree/_scripts/tt_runtime.py +27 -0
- trigger_tree/_scripts/tt_scope.py +76 -0
- trigger_tree/_scripts/validate_palette.js +28 -0
- trigger_tree/cli.py +52 -0
- trigger_tree/hooks/claude-hooks.json +11 -0
- trigger_tree/hooks/hooks.json +9 -0
- trigger_tree-1.14.0.dist-info/METADATA +106 -0
- trigger_tree-1.14.0.dist-info/RECORD +27 -0
- trigger_tree-1.14.0.dist-info/WHEEL +4 -0
- trigger_tree-1.14.0.dist-info/entry_points.txt +2 -0
- trigger_tree-1.14.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""trigger-tree statusline — live doc-discovery stats for the current session.
|
|
3
|
+
|
|
4
|
+
Portable (macOS/Linux), stdlib only. The dot pulses with the age of the last read or scan:
|
|
5
|
+
● bright green < 90s, ◐ amber < 10min, ○ dim otherwise.
|
|
6
|
+
Register in project or user settings under "statusLine" with a refreshInterval.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import unicodedata
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
|
19
|
+
HIST = os.path.join(ROOT, ".trigger-tree", "history.jsonl")
|
|
20
|
+
BADGE = os.path.join(ROOT, ".trigger-tree", "badge.json")
|
|
21
|
+
|
|
22
|
+
RESET = "\033[0m"
|
|
23
|
+
FRESH = "\033[1;38;5;114m" # bright green
|
|
24
|
+
WARM = "\033[38;5;178m" # amber
|
|
25
|
+
COLD = "\033[38;5;245m" # dim
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def terminal_safe(value):
|
|
29
|
+
"""Strip terminal controls and invisible direction overrides from untrusted text."""
|
|
30
|
+
bidi_controls = "\u061c\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069"
|
|
31
|
+
text = re.sub(r"(?:\x1b\]|\x9d).*?(?:\x07|\x1b\\|\x9c)", "", str(value), flags=re.S)
|
|
32
|
+
text = re.sub(r"(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]", "", text)
|
|
33
|
+
text = re.sub(r"\x1b[@-_]", "", text)
|
|
34
|
+
return "".join(
|
|
35
|
+
ch
|
|
36
|
+
for ch in text
|
|
37
|
+
if unicodedata.category(ch) not in ("Cc", "Cs") and ch not in bidi_controls
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
sys.stdout.reconfigure(encoding="utf-8") # emoji-safe on Windows consoles
|
|
43
|
+
except AttributeError: # pragma: no cover — exotic stdout replacement
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
try:
|
|
49
|
+
data = json.load(sys.stdin)
|
|
50
|
+
except (json.JSONDecodeError, ValueError):
|
|
51
|
+
data = {}
|
|
52
|
+
session = data.get("session_id")
|
|
53
|
+
if not session:
|
|
54
|
+
print("🌳 tt: no data")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
files, scans, last, last_time = {}, 0, None, None
|
|
58
|
+
state_name = hashlib.sha256(str(session).encode("utf-8")).hexdigest()[:32] + ".json"
|
|
59
|
+
state_path = os.path.join(ROOT, ".trigger-tree", "sessions", state_name)
|
|
60
|
+
try:
|
|
61
|
+
state = json.loads(open(state_path, encoding="utf-8").read())
|
|
62
|
+
except (OSError, ValueError):
|
|
63
|
+
state = None
|
|
64
|
+
if state is not None:
|
|
65
|
+
files = {path: True for path in state.get("files", [])}
|
|
66
|
+
scans = int(state.get("scans", 0))
|
|
67
|
+
last = state.get("last")
|
|
68
|
+
try:
|
|
69
|
+
last_time = datetime.strptime(last["ts"], "%Y-%m-%dT%H:%M:%SZ").replace(
|
|
70
|
+
tzinfo=timezone.utc
|
|
71
|
+
)
|
|
72
|
+
except (KeyError, TypeError, ValueError):
|
|
73
|
+
last_time = None
|
|
74
|
+
elif not os.path.isfile(HIST):
|
|
75
|
+
print("🌳 tt: no data")
|
|
76
|
+
return
|
|
77
|
+
else:
|
|
78
|
+
fh = open(HIST, encoding="utf-8")
|
|
79
|
+
try:
|
|
80
|
+
scans, last, last_time = _collect_history(fh, session, files)
|
|
81
|
+
finally:
|
|
82
|
+
fh.close()
|
|
83
|
+
|
|
84
|
+
if not files and not scans:
|
|
85
|
+
print("🌳 tt: 0 docs consulted")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
dirs = {os.path.dirname(p) for p in files}
|
|
89
|
+
depth = max((p.count("/") for p in files), default=0)
|
|
90
|
+
grade = mature_grade()
|
|
91
|
+
stats = f"{grade} · " if grade else ""
|
|
92
|
+
stats += f"{len(files)} files · {scans} searches · {len(dirs)} folders · depth {depth}"
|
|
93
|
+
|
|
94
|
+
age = 10**9
|
|
95
|
+
if last_time is not None:
|
|
96
|
+
age = time.time() - last_time.timestamp()
|
|
97
|
+
|
|
98
|
+
if age < 90:
|
|
99
|
+
dot, color = "●", FRESH
|
|
100
|
+
elif age < 600:
|
|
101
|
+
dot, color = "◐", WARM
|
|
102
|
+
else:
|
|
103
|
+
dot, color = "○", COLD
|
|
104
|
+
|
|
105
|
+
path = terminal_safe(last["path"]) + ("/" if last.get("t") == "scan" else "")
|
|
106
|
+
print(f"🌳 {stats} {color}{dot} {path}{RESET}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def mature_grade():
|
|
110
|
+
"""Read the optional public-safe badge cache; measuring badges have no grade."""
|
|
111
|
+
try:
|
|
112
|
+
payload = json.loads(open(BADGE, encoding="utf-8").read())
|
|
113
|
+
except (OSError, ValueError):
|
|
114
|
+
return None
|
|
115
|
+
message = payload.get("message")
|
|
116
|
+
match = re.fullmatch(r"([A-F]) \(\d{1,3}\)", message) if isinstance(message, str) else None
|
|
117
|
+
return match.group(1) if match else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _collect_history(fh, session, files):
|
|
121
|
+
scans, last, last_time = 0, None, None
|
|
122
|
+
for line in fh:
|
|
123
|
+
if f'"session":"{session}"' not in line and f'"session": "{session}"' not in line:
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
event = json.loads(line)
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
continue
|
|
129
|
+
typ = event.get("t")
|
|
130
|
+
if typ == "read":
|
|
131
|
+
files[event["path"]] = True
|
|
132
|
+
elif typ == "scan":
|
|
133
|
+
scans += 1
|
|
134
|
+
else:
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
event_time = datetime.strptime(event["ts"], "%Y-%m-%dT%H:%M:%SZ").replace(
|
|
138
|
+
tzinfo=timezone.utc
|
|
139
|
+
)
|
|
140
|
+
except (KeyError, ValueError):
|
|
141
|
+
event_time = None
|
|
142
|
+
if last is None or (
|
|
143
|
+
event_time is not None and (last_time is None or event_time >= last_time)
|
|
144
|
+
):
|
|
145
|
+
last, last_time = event, event_time
|
|
146
|
+
return scans, last, last_time
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
try:
|
|
151
|
+
main()
|
|
152
|
+
except Exception:
|
|
153
|
+
print("🌳 tt")
|
|
154
|
+
sys.exit(0)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Print concise, deterministic router suggestions; keep full stats off stdout."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def router_for(path, router_coverage=None):
|
|
13
|
+
"""Return a router proven to exist in stats; never invent index.md."""
|
|
14
|
+
folder = path.rsplit("/", 1)[0] if "/" in path else "(root)"
|
|
15
|
+
for item in router_coverage or []:
|
|
16
|
+
if item.get("folder") == folder:
|
|
17
|
+
return item.get("router")
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_suggestions(stats):
|
|
22
|
+
suggestions = []
|
|
23
|
+
seen = set()
|
|
24
|
+
|
|
25
|
+
def add(key, text):
|
|
26
|
+
if key not in seen and len(suggestions) < 5:
|
|
27
|
+
seen.add(key)
|
|
28
|
+
suggestions.append(text)
|
|
29
|
+
|
|
30
|
+
for item in stats.get("review_candidates", []):
|
|
31
|
+
if item.get("classification") == "protected":
|
|
32
|
+
add(
|
|
33
|
+
("protected", item["path"]),
|
|
34
|
+
f"Review, likely keep — rare-but-critical: {item['path']} — {'; '.join(item['why'])}. {item['caveat']}",
|
|
35
|
+
)
|
|
36
|
+
elif (
|
|
37
|
+
not item.get("template", False)
|
|
38
|
+
and item.get("router")
|
|
39
|
+
and not item.get("router_mentions_target", False)
|
|
40
|
+
):
|
|
41
|
+
add(
|
|
42
|
+
("gap", item["path"]),
|
|
43
|
+
f"Review candidate: add a link to {item['path']} in {item['router']} — both files exist and that router does not mention the target. {item['caveat']}",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Pre-1.0 payloads lack enough repository evidence for a safe link edit.
|
|
47
|
+
for item in stats.get("untouched_detail", []) if not stats.get("review_candidates") else []:
|
|
48
|
+
if not item.get("referenced_from") and not item.get("template", False):
|
|
49
|
+
path = item["path"]
|
|
50
|
+
add(
|
|
51
|
+
("gap", path),
|
|
52
|
+
f"Review routing for {path} — legacy telemetry shows no reads or references, but cannot verify an existing router target. Low reads can mean rare-but-critical; verify before editing.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for coverage in stats.get("router_coverage", []):
|
|
56
|
+
router = coverage.get("router")
|
|
57
|
+
for path in coverage.get("unlisted", []):
|
|
58
|
+
add(
|
|
59
|
+
("unlisted", path),
|
|
60
|
+
f"Review candidate: add a link to {path} in {router} — both files exist and the folder router does not mention the target.",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for folder in stats.get("folders", []):
|
|
64
|
+
name = folder["folder"]
|
|
65
|
+
if name.startswith(".claude/") or name == "(root)":
|
|
66
|
+
continue
|
|
67
|
+
if not folder.get("has_index") and folder.get("coverage", 0) < 0.5:
|
|
68
|
+
add(
|
|
69
|
+
("folder", name),
|
|
70
|
+
f"Add a folder entry point in {name}/ (README.md, _index.md, or index.md) — {folder['touched']}/{folder['files']} files read and no existing router was found.",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for folder in stats.get("folders", []):
|
|
74
|
+
name = folder["folder"]
|
|
75
|
+
if (
|
|
76
|
+
folder.get("coverage") == 0
|
|
77
|
+
and name != "(root)"
|
|
78
|
+
and not name.startswith(".claude/")
|
|
79
|
+
and ("folder", name) not in seen
|
|
80
|
+
):
|
|
81
|
+
add(
|
|
82
|
+
("cold", name),
|
|
83
|
+
f"Review routing for {name}/ — none of its {folder['files']} files were read. Low reads can mean rare-but-critical; verify before archiving.",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
for hunt in stats.get("search_activity", stats.get("hunting", [])):
|
|
87
|
+
if hunt.get("pattern") == "distributed":
|
|
88
|
+
add(
|
|
89
|
+
("hunt", hunt["path"]),
|
|
90
|
+
f"Review routing for {hunt['path']} — {hunt['scans']} searches recur across {hunt['sessions']}/{hunt['total_sessions']} sessions ({hunt['tools']}); search telemetry is correlational, not proof of failed routing.",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
for path in stats.get("unknown_reads", []):
|
|
94
|
+
add(
|
|
95
|
+
("unknown", path),
|
|
96
|
+
f"Review inventory configuration for {path} — the file exists and was read, but is outside the current documentation inventory.",
|
|
97
|
+
)
|
|
98
|
+
return suggestions
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
[sys.executable, os.path.join(SCRIPT_DIR, "tt-stats.py")],
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
check=True,
|
|
107
|
+
)
|
|
108
|
+
stats = json.loads(result.stdout)
|
|
109
|
+
totals = stats["totals"]
|
|
110
|
+
if stats["maturity"] == "cold-start":
|
|
111
|
+
print(
|
|
112
|
+
f"🌳 Not enough data yet ({totals['reads']} reads, {stats['sessions']} sessions) — suggestions need a few working sessions first."
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
print(
|
|
116
|
+
f"🌳 Suggestions — based on {totals['reads']} reads, {totals['scans']} searches, {stats['sessions']} sessions; proposes router edits only and changes nothing until you confirm."
|
|
117
|
+
)
|
|
118
|
+
suggestions = build_suggestions(stats)
|
|
119
|
+
if not suggestions:
|
|
120
|
+
print("No evidence-backed router changes found yet.")
|
|
121
|
+
return
|
|
122
|
+
for number, suggestion in enumerate(suggestions, 1):
|
|
123
|
+
print(f"{number}. {suggestion}")
|
|
124
|
+
print("Apply any of these? Reply with the numbers.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Print concise, client-aware instruction-maintenance tips."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def markdown_lines(path):
|
|
10
|
+
try:
|
|
11
|
+
return len(path.read_text(encoding="utf-8").splitlines())
|
|
12
|
+
except (OSError, UnicodeError):
|
|
13
|
+
return 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_paths_frontmatter(path):
|
|
17
|
+
try:
|
|
18
|
+
text = path.read_text(encoding="utf-8")
|
|
19
|
+
except (OSError, UnicodeError):
|
|
20
|
+
return True
|
|
21
|
+
if not text.startswith("---\n"):
|
|
22
|
+
return False
|
|
23
|
+
end = text.find("\n---", 4)
|
|
24
|
+
return end >= 0 and any(line.strip().startswith("paths:") for line in text[4:end].splitlines())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def claude_tips(root):
|
|
28
|
+
tips = [
|
|
29
|
+
"Run /memory periodically and remove stale or contradictory auto-memory entries; memory is editable context, not an enforced source of truth."
|
|
30
|
+
]
|
|
31
|
+
instruction_files = [
|
|
32
|
+
path for path in (root / "CLAUDE.md", root / ".claude" / "CLAUDE.md") if path.is_file()
|
|
33
|
+
]
|
|
34
|
+
if not instruction_files:
|
|
35
|
+
tips.append(
|
|
36
|
+
"Add a project CLAUDE.md with only durable commands, conventions, and architecture facts you repeatedly need to explain."
|
|
37
|
+
)
|
|
38
|
+
for path in instruction_files:
|
|
39
|
+
count = markdown_lines(path)
|
|
40
|
+
if count > 200:
|
|
41
|
+
tips.append(
|
|
42
|
+
f"Trim {path.relative_to(root)} ({count} lines): Anthropic recommends targeting under 200 lines and moving specialized guidance to scoped rules or skills."
|
|
43
|
+
)
|
|
44
|
+
break
|
|
45
|
+
rules_dir = root / ".claude" / "rules"
|
|
46
|
+
unscoped = (
|
|
47
|
+
sorted(
|
|
48
|
+
path.relative_to(root).as_posix()
|
|
49
|
+
for path in rules_dir.rglob("*.md")
|
|
50
|
+
if path.is_file() and not has_paths_frontmatter(path)
|
|
51
|
+
)
|
|
52
|
+
if rules_dir.is_dir()
|
|
53
|
+
else []
|
|
54
|
+
)
|
|
55
|
+
if unscoped:
|
|
56
|
+
examples = ", ".join(unscoped[:2])
|
|
57
|
+
suffix = " …" if len(unscoped) > 2 else ""
|
|
58
|
+
tips.append(
|
|
59
|
+
f"Review {len(unscoped)} always-loaded rule file(s) for path scoping ({examples}{suffix}); unconditional rules consume context in every session."
|
|
60
|
+
)
|
|
61
|
+
return tips[:4]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def codex_tips(root):
|
|
65
|
+
agents = root / "AGENTS.md"
|
|
66
|
+
tips = []
|
|
67
|
+
if not agents.is_file():
|
|
68
|
+
tips.append(
|
|
69
|
+
"Add AGENTS.md with repository navigation, build/test commands, conventions, and non-obvious constraints that Codex cannot infer reliably."
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
tips.append(
|
|
73
|
+
"Review AGENTS.md after architecture or workflow changes so persistent Codex instructions do not drift from the repository."
|
|
74
|
+
)
|
|
75
|
+
text = agents.read_text(encoding="utf-8", errors="replace").lower()
|
|
76
|
+
if not any(token in text for token in ("test", "check", "verify", "ci")):
|
|
77
|
+
tips.append(
|
|
78
|
+
"Add exact verification commands to AGENTS.md; reproducible environment and test instructions reduce avoidable agent errors."
|
|
79
|
+
)
|
|
80
|
+
tips.append(
|
|
81
|
+
"Keep agent-facing indexes and commands executable and inspectable; use trigger-tree suggestions for evidence-backed navigation gaps."
|
|
82
|
+
)
|
|
83
|
+
return tips[:4]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def tips_for(client, root):
|
|
87
|
+
root = Path(root).resolve()
|
|
88
|
+
return claude_tips(root) if client == "claude" else codex_tips(root)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_args(argv=None):
|
|
92
|
+
parser = argparse.ArgumentParser()
|
|
93
|
+
parser.add_argument("--client", required=True, choices=("claude", "codex"))
|
|
94
|
+
parser.add_argument("--project", default=os.getcwd())
|
|
95
|
+
return parser.parse_args(argv)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main(argv=None):
|
|
99
|
+
args = parse_args(argv)
|
|
100
|
+
tips = tips_for(args.client, args.project)
|
|
101
|
+
print(f"🌳 {args.client.title()} maintenance tips — review only; nothing was changed.")
|
|
102
|
+
for number, tip in enumerate(tips, 1):
|
|
103
|
+
print(f"{number}. {tip}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Remove trigger-tree project wiring without deleting telemetry or ignore rules."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import runpy
|
|
7
|
+
|
|
8
|
+
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
|
9
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
SETUP = runpy.run_path(os.path.join(SCRIPT_DIR, "tt-setup.py"))
|
|
11
|
+
assert_safe_destination = SETUP["assert_safe_destination"]
|
|
12
|
+
atomic_write = SETUP["atomic_write"]
|
|
13
|
+
report = SETUP["report"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def unregister_statusline():
|
|
17
|
+
path = os.path.join(ROOT, ".claude", "settings.json")
|
|
18
|
+
assert_safe_destination(path)
|
|
19
|
+
if not os.path.isfile(path):
|
|
20
|
+
report("skipped", ".claude/settings.json (absent)")
|
|
21
|
+
return
|
|
22
|
+
try:
|
|
23
|
+
settings = json.load(open(path, encoding="utf-8"))
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
report("skipped", ".claude/settings.json (unparseable — remove statusLine manually)")
|
|
26
|
+
return
|
|
27
|
+
status = settings.get("statusLine")
|
|
28
|
+
command = status.get("command", "") if isinstance(status, dict) else ""
|
|
29
|
+
if "tt-statusline.py" not in command:
|
|
30
|
+
report("skipped", ".claude/settings.json (foreign statusLine left untouched)")
|
|
31
|
+
return
|
|
32
|
+
del settings["statusLine"]
|
|
33
|
+
atomic_write(path, json.dumps(settings, indent=2) + "\n")
|
|
34
|
+
report("updated", ".claude/settings.json (trigger-tree statusLine removed)")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def remove_statusline_script():
|
|
38
|
+
path = os.path.join(ROOT, ".claude", "tt-statusline.py")
|
|
39
|
+
assert_safe_destination(path)
|
|
40
|
+
if not os.path.isfile(path):
|
|
41
|
+
report("skipped", ".claude/tt-statusline.py (absent)")
|
|
42
|
+
return
|
|
43
|
+
os.unlink(path)
|
|
44
|
+
report("removed", ".claude/tt-statusline.py")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
unregister_statusline()
|
|
49
|
+
remove_statusline_script()
|
|
50
|
+
print(
|
|
51
|
+
"kept .trigger-tree/ telemetry and .gitignore entries; "
|
|
52
|
+
"delete them manually if you want to erase local history"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
try:
|
|
58
|
+
main()
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
print(f"skipped uninstall ({exc})")
|