usage-cli 0.29.32__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.
Files changed (109) hide show
  1. adapters/__init__.py +5 -0
  2. adapters/agy.py +68 -0
  3. adapters/claude.py +215 -0
  4. adapters/codex.py +209 -0
  5. adapters/rate_limits.py +76 -0
  6. adapters/registry.py +17 -0
  7. adapters/types.py +139 -0
  8. agy_disk_cache.py +135 -0
  9. agy_loader.py +416 -0
  10. agy_quota_probe.py +748 -0
  11. agy_window_keeper.py +185 -0
  12. analyzer/__init__.py +5 -0
  13. analyzer/aggregator.py +139 -0
  14. analyzer/blocks.py +80 -0
  15. analyzer/diagnoser.py +638 -0
  16. analyzer/insights.py +277 -0
  17. analyzer/persona_loader.py +199 -0
  18. analyzer/reporter.py +989 -0
  19. analyzer/subscription.py +108 -0
  20. burn_rate.py +75 -0
  21. cache_quarantine.py +50 -0
  22. codex_disk_cache.py +227 -0
  23. codex_events.py +136 -0
  24. codex_fork_replay.py +111 -0
  25. codex_loader.py +1426 -0
  26. codex_paths.py +20 -0
  27. critter_frames.py +26 -0
  28. discussion_bridge.py +1196 -0
  29. discussion_cli.py +844 -0
  30. discussion_session.py +622 -0
  31. discussion_usage.py +13 -0
  32. discussion_window.py +955 -0
  33. disk_cache_common.py +132 -0
  34. disk_cache_lifecycle.py +39 -0
  35. doctor.py +452 -0
  36. fsevents_watch.py +207 -0
  37. history_disk_cache.py +110 -0
  38. history_loader.py +416 -0
  39. i18n.py +88 -0
  40. jsonl_limits.py +17 -0
  41. jsonl_utils.py +40 -0
  42. login_item.py +154 -0
  43. main.py +387 -0
  44. menubar.py +1201 -0
  45. menubar_actions.py +204 -0
  46. menubar_agy.py +193 -0
  47. menubar_chrome.py +156 -0
  48. menubar_menu.py +169 -0
  49. menubar_notify.py +102 -0
  50. menubar_popover.py +233 -0
  51. menubar_prefs.py +118 -0
  52. menubar_refresh.py +285 -0
  53. menubar_state.py +1200 -0
  54. menubar_title.py +157 -0
  55. menubar_update.py +123 -0
  56. panel_window.py +78 -0
  57. panel_window_state.py +159 -0
  58. panels/__init__.py +186 -0
  59. panels/base.py +83 -0
  60. panels/dynamic_height.py +140 -0
  61. panels/payload.py +178 -0
  62. panels/web_panel.py +513 -0
  63. panels/window_drag.py +56 -0
  64. prefs.py +44 -0
  65. pricing.py +452 -0
  66. project_resolver.py +112 -0
  67. service_status.py +383 -0
  68. session_hooks.py +1154 -0
  69. setup_app.py +171 -0
  70. setup_hook.py +1011 -0
  71. statusline_settings.py +160 -0
  72. talent_market_bridge.py +243 -0
  73. time_utils.py +24 -0
  74. tui.py +288 -0
  75. tui_sprite.py +206 -0
  76. ui/__init__.py +5 -0
  77. ui/html_report.py +923 -0
  78. ui/report_scripts.py +251 -0
  79. ui/report_styles.py +370 -0
  80. ui/tables.py +888 -0
  81. update_checker.py +156 -0
  82. update_gate.py +66 -0
  83. update_release_notes.py +49 -0
  84. usage_cli-0.29.32.data/data/share/usage/i18n.json +2427 -0
  85. usage_cli-0.29.32.dist-info/METADATA +223 -0
  86. usage_cli-0.29.32.dist-info/RECORD +109 -0
  87. usage_cli-0.29.32.dist-info/WHEEL +5 -0
  88. usage_cli-0.29.32.dist-info/entry_points.txt +3 -0
  89. usage_cli-0.29.32.dist-info/licenses/LICENSE +663 -0
  90. usage_cli-0.29.32.dist-info/top_level.txt +80 -0
  91. usage_cli.py +827 -0
  92. usage_client.py +487 -0
  93. usage_diagnosis_snapshot.py +143 -0
  94. usage_dir_sweeper.py +100 -0
  95. usage_lang.py +79 -0
  96. usage_logging.py +75 -0
  97. usage_notifications.py +96 -0
  98. usage_rate.py +97 -0
  99. usage_session_resume.py +913 -0
  100. usage_statusline.py +810 -0
  101. usage_statusline_agy.py +397 -0
  102. usage_statusline_forwarder.py +88 -0
  103. usage_terse_mode.py +223 -0
  104. usage_terse_reminder.py +151 -0
  105. win_login_item.py +53 -0
  106. window_keeper.py +264 -0
  107. windows_watch.py +443 -0
  108. wintray.py +2014 -0
  109. wintray_menu.py +136 -0
session_hooks.py ADDED
@@ -0,0 +1,1154 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
3
+ #
4
+ # Part of "usage". Free software licensed under the GNU Affero General Public
5
+ # License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
6
+
7
+ """Install, configure, and repair usage's session companion hooks."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import io
13
+ import json
14
+ import os
15
+ import re
16
+ import shutil
17
+ import stat
18
+ import sys
19
+ from datetime import UTC, datetime
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ import setup_hook
24
+ from codex_paths import codex_home
25
+ from i18n import t as _t
26
+ from setup_hook import (
27
+ BACKUP_KEY,
28
+ HOOK_VERSION,
29
+ _atomic_write_text,
30
+ _copy_forwarder_script,
31
+ _copy_hook_script,
32
+ _detect_current_state,
33
+ _ensure_table_line,
34
+ _find_system_python,
35
+ _forwarder_command,
36
+ _installed_hook_version,
37
+ _load_settings,
38
+ _read_codex_config,
39
+ _save_settings,
40
+ _shell_arg,
41
+ _statusline_command,
42
+ _statusline_command_target_exists,
43
+ _uses_bundled_app_python,
44
+ is_setup,
45
+ needs_update,
46
+ setup,
47
+ update_hook,
48
+ )
49
+ from usage_statusline import _exclusive_lock
50
+
51
+ CLAUDE_SETTINGS = setup_hook.CLAUDE_SETTINGS
52
+ CODEX_CONFIG = setup_hook.CODEX_CONFIG
53
+
54
+ # Ceiling C — opt-in SessionStart hook that injects "where you left off" into a new
55
+ # session. Off by default: enabled only via the menu toggle, never by self_heal.
56
+ RESUME_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-session-resume.py"))
57
+ RESUME_PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-resume-prompt.json"))
58
+ RESUME_HOOK_VERSION = "1.6"
59
+ RESUME_MATCHER = "startup|clear"
60
+ RESUME_LANGS = ("zh-TW", "zh-CN", "en", "ja", "ko")
61
+ _RESUME_MARKER = "usage-session-resume"
62
+ _RESUME_MARKERS = (_RESUME_MARKER, "usage_session_resume")
63
+ _RESUME_DIAGNOSIS_CAUSE_KEYS = (
64
+ "repeated_reads",
65
+ "polluter_dirs",
66
+ "anomaly_session",
67
+ "noisy_bash",
68
+ "repeated_bash",
69
+ )
70
+ TERSE_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-terse-mode.py"))
71
+ TERSE_PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-terse-prompt.json"))
72
+ TERSE_HOOK_VERSION = "1.0"
73
+ TERSE_MATCHER = "startup|clear"
74
+ TERSE_LANGS = ("zh-TW", "zh-CN", "en", "ja", "ko")
75
+ _TERSE_MARKER = "usage-terse-mode"
76
+ _TERSE_MARKERS = (_TERSE_MARKER, "usage_terse_mode")
77
+ # Codex CLI shares the same terse-mode script (its SessionStart hook I/O schema matches Claude
78
+ # Code's). Installed into the user-global ~/.codex so one toggle covers both tools.
79
+ CODEX_TERSE_HOOK_TARGET = codex_home() / "usage-terse-mode.py"
80
+ CODEX_HOOKS_JSON = codex_home() / "hooks.json"
81
+ CODEX_TERSE_MATCHER = "startup|resume|clear"
82
+ _FEATURES_HOOKS_REGEX = re.compile(r"(?m)^[ \t]*hooks\s*=\s*[A-Za-z0-9_]+")
83
+ # Per-message tail reminder — a UserPromptSubmit hook installed alongside the SessionStart
84
+ # terse hook. Re-injects a one-line nudge on every prompt so the terse style holds across a
85
+ # long conversation. Claude Code only — Codex CLI has no UserPromptSubmit equivalent.
86
+ TERSE_REMINDER_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-terse-reminder.py"))
87
+ TERSE_REMINDER_MATCHER = ""
88
+ _TERSE_REMINDER_MARKER = "usage-terse-reminder"
89
+ _TERSE_REMINDER_MARKERS = (_TERSE_REMINDER_MARKER, "usage_terse_reminder")
90
+
91
+
92
+ def _migrate_bundled_python_commands_if_needed(
93
+ settings: dict[str, Any] | None = None,
94
+ ) -> None:
95
+ data = _load_settings() if settings is None else settings
96
+ changed = False
97
+ details: list[str] = []
98
+
99
+ sl = data.get("statusLine")
100
+ if isinstance(sl, dict):
101
+ command = sl.get("command")
102
+ if isinstance(command, str) and _uses_bundled_app_python(command):
103
+ if "usage-statusline-forwarder" in command:
104
+ new_command = _forwarder_command()
105
+ if command != new_command:
106
+ sl["command"] = new_command
107
+ changed = True
108
+ details.append("statusLine=forwarder")
109
+ elif "usage-statusline" in command:
110
+ new_command = _statusline_command()
111
+ if command != new_command:
112
+ sl["command"] = new_command
113
+ changed = True
114
+ details.append("statusLine=direct")
115
+
116
+ entries = _session_start_list(data)
117
+ if entries:
118
+ new_command = _resume_command()
119
+ resume_changed = False
120
+ for entry in entries:
121
+ if not isinstance(entry, dict) or not _is_resume_entry(entry):
122
+ continue
123
+ hooks = entry.get("hooks")
124
+ if not isinstance(hooks, list):
125
+ continue
126
+ for hook in hooks:
127
+ if not isinstance(hook, dict):
128
+ continue
129
+ command = hook.get("command")
130
+ if not isinstance(command, str) or not any(
131
+ marker in command for marker in _RESUME_MARKERS
132
+ ):
133
+ continue
134
+ if command != new_command:
135
+ hook["command"] = new_command
136
+ changed = True
137
+ resume_changed = True
138
+ if resume_changed:
139
+ details.append("resume")
140
+
141
+ new_command = _terse_command()
142
+ terse_changed = False
143
+ for entry in entries:
144
+ if not isinstance(entry, dict) or not _is_terse_entry(entry):
145
+ continue
146
+ hooks = entry.get("hooks")
147
+ if not isinstance(hooks, list):
148
+ continue
149
+ for hook in hooks:
150
+ if not isinstance(hook, dict):
151
+ continue
152
+ command = hook.get("command")
153
+ if not isinstance(command, str) or not any(
154
+ marker in command for marker in _TERSE_MARKERS
155
+ ):
156
+ continue
157
+ if command != new_command:
158
+ hook["command"] = new_command
159
+ changed = True
160
+ terse_changed = True
161
+ if terse_changed:
162
+ details.append("terse")
163
+
164
+ hooks_data = data.get("hooks")
165
+ prompt_entries = hooks_data.get("UserPromptSubmit") if isinstance(hooks_data, dict) else None
166
+ if isinstance(prompt_entries, list):
167
+ new_command = _terse_reminder_command()
168
+ reminder_changed = False
169
+ for entry in prompt_entries:
170
+ if not isinstance(entry, dict) or not _is_terse_reminder_entry(entry):
171
+ continue
172
+ hooks = entry.get("hooks")
173
+ if not isinstance(hooks, list):
174
+ continue
175
+ for hook in hooks:
176
+ if not isinstance(hook, dict):
177
+ continue
178
+ command = hook.get("command")
179
+ if not isinstance(command, str) or not any(
180
+ marker in command for marker in _TERSE_REMINDER_MARKERS
181
+ ):
182
+ continue
183
+ if command != new_command:
184
+ hook["command"] = new_command
185
+ changed = True
186
+ reminder_changed = True
187
+ if reminder_changed:
188
+ details.append("terse_reminder")
189
+
190
+ if not changed:
191
+ return
192
+ _save_settings(data)
193
+ _append_self_heal_log("migrate_bundled_python", ", ".join(details))
194
+
195
+
196
+ def _resolve_resume_source() -> Path:
197
+ paths = [
198
+ Path(__file__).resolve().parent / "usage_session_resume.py",
199
+ Path(sys.executable).resolve().parent.parent / "Resources" / "usage_session_resume.py",
200
+ ]
201
+ for path in paths:
202
+ if path.exists():
203
+ return path
204
+ tried = ", ".join(str(path) for path in paths)
205
+ raise SystemExit(_t("setup_resume_source_missing", tried=tried))
206
+
207
+
208
+ def _resolve_terse_source() -> Path:
209
+ paths = [
210
+ Path(__file__).resolve().parent / "usage_terse_mode.py",
211
+ Path(sys.executable).resolve().parent.parent / "Resources" / "usage_terse_mode.py",
212
+ ]
213
+ for path in paths:
214
+ if path.exists():
215
+ return path
216
+ tried = ", ".join(str(path) for path in paths)
217
+ raise SystemExit(_t("setup_terse_source_missing", tried=tried))
218
+
219
+
220
+ def _resolve_terse_reminder_source() -> Path:
221
+ paths = [
222
+ Path(__file__).resolve().parent / "usage_terse_reminder.py",
223
+ (
224
+ Path(sys.executable).resolve().parent.parent
225
+ / "Resources"
226
+ / "usage_terse_reminder.py"
227
+ ),
228
+ ]
229
+ for path in paths:
230
+ if path.exists():
231
+ return path
232
+ tried = ", ".join(str(path) for path in paths)
233
+ raise SystemExit(_t("setup_terse_source_missing", tried=tried))
234
+
235
+
236
+ def _resume_command() -> str:
237
+ python = _find_system_python()
238
+ return f"{_shell_arg(python)} {_shell_arg(str(RESUME_HOOK_TARGET))}"
239
+
240
+
241
+ def _terse_command() -> str:
242
+ python = _find_system_python()
243
+ return f"{_shell_arg(python)} {_shell_arg(str(TERSE_HOOK_TARGET))}"
244
+
245
+
246
+ def _terse_reminder_command() -> str:
247
+ python = _find_system_python()
248
+ return f"{_shell_arg(python)} {_shell_arg(str(TERSE_REMINDER_HOOK_TARGET))}"
249
+
250
+
251
+ def _copy_resume_script() -> None:
252
+ source = _resolve_resume_source()
253
+ RESUME_HOOK_TARGET.parent.mkdir(parents=True, exist_ok=True)
254
+ shutil.copyfile(source, RESUME_HOOK_TARGET)
255
+ RESUME_HOOK_TARGET.chmod(
256
+ RESUME_HOOK_TARGET.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
257
+ )
258
+
259
+
260
+ def _copy_terse_script() -> None:
261
+ source = _resolve_terse_source()
262
+ TERSE_HOOK_TARGET.parent.mkdir(parents=True, exist_ok=True)
263
+ shutil.copyfile(source, TERSE_HOOK_TARGET)
264
+ TERSE_HOOK_TARGET.chmod(
265
+ TERSE_HOOK_TARGET.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
266
+ )
267
+
268
+
269
+ def _copy_terse_reminder_script() -> None:
270
+ source = _resolve_terse_reminder_source()
271
+ TERSE_REMINDER_HOOK_TARGET.parent.mkdir(parents=True, exist_ok=True)
272
+ shutil.copyfile(source, TERSE_REMINDER_HOOK_TARGET)
273
+ TERSE_REMINDER_HOOK_TARGET.chmod(
274
+ TERSE_REMINDER_HOOK_TARGET.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
275
+ )
276
+
277
+
278
+ def _write_resume_sidecar() -> None:
279
+ """Mirror i18n.json's rw_prompt/rw_none into a sidecar the stdlib hook can read,
280
+ so the injected wording stays single-sourced and the hook needs no app imports."""
281
+ from i18n import I18N_PATH
282
+
283
+ try:
284
+ bundle = json.loads(I18N_PATH.read_text(encoding="utf-8"))
285
+ except (OSError, json.JSONDecodeError):
286
+ return
287
+ if not isinstance(bundle, dict):
288
+ return
289
+ en_raw = bundle.get("en")
290
+ en: dict[str, Any] = en_raw if isinstance(en_raw, dict) else {}
291
+ out: dict[str, dict[str, Any]] = {}
292
+ for lang in RESUME_LANGS:
293
+ table_raw = bundle.get(lang)
294
+ table: dict[str, Any] = table_raw if isinstance(table_raw, dict) else {}
295
+ prompt = table.get("report_rw_prompt") or en.get("report_rw_prompt")
296
+ none_label = table.get("report_rw_none") or en.get("report_rw_none")
297
+ lead = table.get("report_rw_inject_lead") or en.get("report_rw_inject_lead") or ""
298
+ empty = table.get("report_rw_empty") or en.get("report_rw_empty") or ""
299
+ uncommitted = table.get("report_rw_uncommitted") or en.get("report_rw_uncommitted") or ""
300
+ diagnosis_reminder = (
301
+ table.get("report_rw_diagnosis_reminder")
302
+ or en.get("report_rw_diagnosis_reminder")
303
+ or ""
304
+ )
305
+ diagnosis_reminder_explain = (
306
+ table.get("report_rw_diagnosis_reminder_explain")
307
+ or en.get("report_rw_diagnosis_reminder_explain")
308
+ or ""
309
+ )
310
+ diagnosis_default_cause = (
311
+ table.get("report_rw_diagnosis_cause_default")
312
+ or en.get("report_rw_diagnosis_cause_default")
313
+ or ""
314
+ )
315
+ if isinstance(prompt, str) and isinstance(none_label, str):
316
+ diagnosis_causes = {
317
+ key: (
318
+ table.get(f"report_rw_diagnosis_cause_{key}")
319
+ or en.get(f"report_rw_diagnosis_cause_{key}")
320
+ or diagnosis_default_cause
321
+ )
322
+ for key in _RESUME_DIAGNOSIS_CAUSE_KEYS
323
+ }
324
+ out[lang] = {
325
+ "prompt": prompt,
326
+ "none": none_label,
327
+ "lead": lead,
328
+ "empty": empty,
329
+ "uncommitted": uncommitted,
330
+ "diagnosis_reminder": diagnosis_reminder,
331
+ "diagnosis_reminder_explain": diagnosis_reminder_explain,
332
+ "diagnosis_default_cause": diagnosis_default_cause,
333
+ "diagnosis_causes": diagnosis_causes,
334
+ }
335
+ if out:
336
+ RESUME_PROMPT_SIDECAR.parent.mkdir(parents=True, exist_ok=True)
337
+ _atomic_write_text(
338
+ RESUME_PROMPT_SIDECAR, json.dumps(out, ensure_ascii=False, indent=2) + "\n"
339
+ )
340
+
341
+
342
+ def _write_terse_sidecar() -> None:
343
+ from i18n import I18N_PATH
344
+
345
+ try:
346
+ bundle = json.loads(I18N_PATH.read_text(encoding="utf-8"))
347
+ except (OSError, json.JSONDecodeError):
348
+ return
349
+ if not isinstance(bundle, dict):
350
+ return
351
+ en_raw = bundle.get("en")
352
+ en: dict[str, Any] = en_raw if isinstance(en_raw, dict) else {}
353
+ out: dict[str, dict[str, str]] = {}
354
+ for lang in TERSE_LANGS:
355
+ table_raw = bundle.get(lang)
356
+ table: dict[str, Any] = table_raw if isinstance(table_raw, dict) else {}
357
+ instruction = table.get("terse_mode_instruction") or en.get("terse_mode_instruction")
358
+ reminder = table.get("terse_reminder_instruction") or en.get("terse_reminder_instruction")
359
+ if isinstance(instruction, str) and instruction:
360
+ entry = {"instruction": instruction}
361
+ if isinstance(reminder, str) and reminder:
362
+ entry["reminder"] = reminder
363
+ out[lang] = entry
364
+ if out:
365
+ TERSE_PROMPT_SIDECAR.parent.mkdir(parents=True, exist_ok=True)
366
+ _atomic_write_text(
367
+ TERSE_PROMPT_SIDECAR, json.dumps(out, ensure_ascii=False, indent=2) + "\n"
368
+ )
369
+
370
+
371
+ def _is_hook_entry(entry: object, markers: tuple[str, ...]) -> bool:
372
+ if not isinstance(entry, dict):
373
+ return False
374
+ hooks = entry.get("hooks")
375
+ if not isinstance(hooks, list):
376
+ return False
377
+ return any(
378
+ isinstance(h, dict)
379
+ and isinstance(h.get("command"), str)
380
+ and any(marker in h["command"] for marker in markers)
381
+ for h in hooks
382
+ )
383
+
384
+
385
+ def _strip_hook_entries(entry: object, markers: tuple[str, ...]) -> object | None:
386
+ if not isinstance(entry, dict):
387
+ return entry
388
+ hooks = entry.get("hooks")
389
+ if not isinstance(hooks, list):
390
+ return entry
391
+ kept = [
392
+ h
393
+ for h in hooks
394
+ if not (
395
+ isinstance(h, dict)
396
+ and isinstance(h.get("command"), str)
397
+ and any(marker in h["command"] for marker in markers)
398
+ )
399
+ ]
400
+ if len(kept) == len(hooks):
401
+ return entry
402
+ if not kept:
403
+ return None
404
+ return {**entry, "hooks": kept}
405
+
406
+
407
+ def _is_resume_entry(entry: object) -> bool:
408
+ return _is_hook_entry(entry, _RESUME_MARKERS)
409
+
410
+
411
+ def _strip_resume_hooks(entry: object) -> object | None:
412
+ """Return ``entry`` with usage-owned resume hooks removed.
413
+
414
+ Removes only the resume hook *item*, not the whole entry, so a user who put their
415
+ own hook in the same SessionStart entry doesn't lose it when we disable. Returns
416
+ ``None`` when nothing but our hook was in the entry, ``entry`` unchanged when it
417
+ held no resume hook.
418
+ """
419
+ return _strip_hook_entries(entry, _RESUME_MARKERS)
420
+
421
+
422
+ def _is_terse_entry(entry: object) -> bool:
423
+ return _is_hook_entry(entry, _TERSE_MARKERS)
424
+
425
+
426
+ def _strip_terse_hooks(entry: object) -> object | None:
427
+ return _strip_hook_entries(entry, _TERSE_MARKERS)
428
+
429
+
430
+ def _user_prompt_submit_list(settings: dict[str, Any]) -> list[Any] | None:
431
+ hooks = settings.get("hooks")
432
+ if not isinstance(hooks, dict):
433
+ return None
434
+ ups = hooks.get("UserPromptSubmit")
435
+ return ups if isinstance(ups, list) else None
436
+
437
+
438
+ def _is_terse_reminder_entry(entry: object) -> bool:
439
+ return _is_hook_entry(entry, _TERSE_REMINDER_MARKERS)
440
+
441
+
442
+ def _strip_terse_reminder_hooks(entry: object) -> object | None:
443
+ return _strip_hook_entries(entry, _TERSE_REMINDER_MARKERS)
444
+
445
+
446
+ def _register_terse_reminder(settings: dict[str, Any]) -> None:
447
+ """Idempotently add the per-message UserPromptSubmit reminder entry to ``settings``
448
+ (in memory — caller persists). Strips any prior reminder entry first so a repeat
449
+ enable never duplicates it."""
450
+ hooks = settings.get("hooks")
451
+ if not isinstance(hooks, dict):
452
+ hooks = {}
453
+ settings["hooks"] = hooks
454
+ ups = hooks.get("UserPromptSubmit")
455
+ if not isinstance(ups, list):
456
+ ups = []
457
+ hooks["UserPromptSubmit"] = ups
458
+ ups[:] = [e for e in (_strip_terse_reminder_hooks(e) for e in ups) if e is not None]
459
+ ups.append(
460
+ {
461
+ "matcher": TERSE_REMINDER_MATCHER,
462
+ "hooks": [{"type": "command", "command": _terse_reminder_command()}],
463
+ }
464
+ )
465
+
466
+
467
+ def _codex_terse_command() -> str:
468
+ python = _find_system_python()
469
+ return f"{_shell_arg(python)} {_shell_arg(str(CODEX_TERSE_HOOK_TARGET))}"
470
+
471
+
472
+ def _load_codex_hooks() -> dict[str, Any] | None:
473
+ if not CODEX_HOOKS_JSON.exists():
474
+ return {}
475
+ try:
476
+ data = json.loads(CODEX_HOOKS_JSON.read_text(encoding="utf-8"))
477
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
478
+ return None
479
+ return data if isinstance(data, dict) else None
480
+
481
+
482
+ def _save_codex_hooks(data: dict[str, Any]) -> None:
483
+ _atomic_write_text(
484
+ CODEX_HOOKS_JSON, json.dumps(data, indent=2, ensure_ascii=False) + "\n"
485
+ )
486
+
487
+
488
+ def _codex_session_start_list(data: dict[str, Any]) -> list[Any] | None:
489
+ hooks = data.get("hooks")
490
+ if not isinstance(hooks, dict):
491
+ return None
492
+ session_start = hooks.get("SessionStart")
493
+ return session_start if isinstance(session_start, list) else None
494
+
495
+
496
+ def _is_codex_terse_installed() -> bool:
497
+ data = _load_codex_hooks()
498
+ if data is None:
499
+ return False
500
+ entries = _codex_session_start_list(data)
501
+ if not entries:
502
+ return False
503
+ return any(_is_terse_entry(e) for e in entries)
504
+
505
+
506
+ def _copy_codex_terse_script() -> None:
507
+ source = _resolve_terse_source()
508
+ CODEX_TERSE_HOOK_TARGET.parent.mkdir(parents=True, exist_ok=True)
509
+ shutil.copyfile(source, CODEX_TERSE_HOOK_TARGET)
510
+ CODEX_TERSE_HOOK_TARGET.chmod(
511
+ CODEX_TERSE_HOOK_TARGET.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
512
+ )
513
+
514
+
515
+ def _setup_codex_terse() -> None:
516
+ """Install the terse hook into the user-global ~/.codex. No-op when config.toml can't be
517
+ read — the [features] hooks flag is the gate, so without a readable config the hook
518
+ wouldn't fire anyway. Stays silent: Codex is an opt-in add-on to the Claude toggle."""
519
+ result = _read_codex_config()
520
+ if result is None:
521
+ return
522
+ content, parsed = result
523
+ data = _load_codex_hooks()
524
+ if data is None:
525
+ print(_t("terse_codex_hooks_unreadable"))
526
+ return
527
+ _copy_codex_terse_script()
528
+
529
+ features = parsed.get("features")
530
+ if not (isinstance(features, dict) and features.get("hooks") is True):
531
+ new_content = _ensure_table_line(
532
+ content, "features", _FEATURES_HOOKS_REGEX, "hooks = true"
533
+ )
534
+ if new_content != content:
535
+ _atomic_write_text(CODEX_CONFIG, new_content)
536
+
537
+ hooks = data.get("hooks")
538
+ if not isinstance(hooks, dict):
539
+ hooks = {}
540
+ data["hooks"] = hooks
541
+ session_start = hooks.get("SessionStart")
542
+ if not isinstance(session_start, list):
543
+ session_start = []
544
+ hooks["SessionStart"] = session_start
545
+ session_start[:] = [
546
+ e for e in (_strip_terse_hooks(e) for e in session_start) if e is not None
547
+ ]
548
+ session_start.append(
549
+ {
550
+ "matcher": CODEX_TERSE_MATCHER,
551
+ "hooks": [{"type": "command", "command": _codex_terse_command(), "timeout": 5}],
552
+ }
553
+ )
554
+ _save_codex_hooks(data)
555
+
556
+
557
+ def _teardown_codex_terse() -> None:
558
+ """Remove only usage's own Codex SessionStart entry; leave ``[features] hooks = true``
559
+ and any user-installed Codex hooks intact. Deletes hooks.json when nothing remains."""
560
+ data = _load_codex_hooks()
561
+ if data is not None:
562
+ entries = _codex_session_start_list(data)
563
+ if entries is not None:
564
+ kept = [e for e in (_strip_terse_hooks(e) for e in entries) if e is not None]
565
+ hooks = data.get("hooks")
566
+ if isinstance(hooks, dict):
567
+ if kept:
568
+ hooks["SessionStart"] = kept
569
+ else:
570
+ hooks.pop("SessionStart", None)
571
+ if not hooks:
572
+ data.pop("hooks", None)
573
+ if data:
574
+ _save_codex_hooks(data)
575
+ else:
576
+ CODEX_HOOKS_JSON.unlink(missing_ok=True)
577
+ CODEX_TERSE_HOOK_TARGET.unlink(missing_ok=True)
578
+
579
+
580
+ def _installed_codex_terse_version() -> str | None:
581
+ try:
582
+ with CODEX_TERSE_HOOK_TARGET.open(encoding="utf-8") as f:
583
+ for line in f:
584
+ if line.startswith("__version__"):
585
+ return line.split("=", 1)[1].strip().strip("\"'")
586
+ except OSError:
587
+ pass
588
+ return None
589
+
590
+
591
+ def _self_heal_codex_terse() -> None:
592
+ """Restore the Codex side of terse mode — but only when Claude's side is already on and
593
+ Codex is installed. Re-runs the idempotent installer for any missing/stale artifact."""
594
+ if not CODEX_CONFIG.exists():
595
+ return
596
+ result = _read_codex_config()
597
+ if result is None:
598
+ return
599
+ _, parsed = result
600
+ features = parsed.get("features")
601
+ hooks_enabled = isinstance(features, dict) and features.get("hooks") is True
602
+
603
+ script_missing = not CODEX_TERSE_HOOK_TARGET.exists()
604
+ entry_missing = not _is_codex_terse_installed()
605
+ old_version = _installed_codex_terse_version()
606
+ version_stale = CODEX_TERSE_HOOK_TARGET.exists() and old_version != TERSE_HOOK_VERSION
607
+
608
+ if not (script_missing or entry_missing or version_stale or not hooks_enabled):
609
+ return
610
+
611
+ _setup_codex_terse()
612
+ if script_missing or entry_missing or not hooks_enabled:
613
+ parts: list[str] = []
614
+ if script_missing:
615
+ parts.append("script")
616
+ if entry_missing:
617
+ parts.append("hooks_entry")
618
+ if not hooks_enabled:
619
+ parts.append("features_flag")
620
+ _append_self_heal_log("restore_terse_hook_codex", f"missing={','.join(parts)}")
621
+ else:
622
+ _append_self_heal_log(
623
+ "update_terse_hook_codex", f"{old_version or 'unknown'} -> {TERSE_HOOK_VERSION}"
624
+ )
625
+
626
+
627
+ def _session_start_list(settings: dict[str, Any]) -> list[Any] | None:
628
+ hooks = settings.get("hooks")
629
+ if not isinstance(hooks, dict):
630
+ return None
631
+ session_start = hooks.get("SessionStart")
632
+ return session_start if isinstance(session_start, list) else None
633
+
634
+
635
+ def is_resume_enabled() -> bool:
636
+ try:
637
+ settings = _load_settings()
638
+ except SystemExit:
639
+ return False
640
+ entries = _session_start_list(settings)
641
+ if not entries:
642
+ return False
643
+ return any(_is_resume_entry(e) for e in entries)
644
+
645
+
646
+ def is_terse_mode_enabled() -> bool:
647
+ try:
648
+ settings = _load_settings()
649
+ except SystemExit:
650
+ return False
651
+ entries = _session_start_list(settings)
652
+ if not entries:
653
+ return False
654
+ return any(_is_terse_entry(e) for e in entries)
655
+
656
+
657
+ def is_terse_reminder_enabled() -> bool:
658
+ try:
659
+ settings = _load_settings()
660
+ except SystemExit:
661
+ return False
662
+ entries = _user_prompt_submit_list(settings)
663
+ if not entries:
664
+ return False
665
+ return any(_is_terse_reminder_entry(e) for e in entries)
666
+
667
+
668
+ def enable_session_resume() -> int:
669
+ setup_hook.configure_windows_utf8_output()
670
+ if not CLAUDE_SETTINGS.parent.exists():
671
+ print(_t("setup_no_agents"), file=sys.stderr)
672
+ return 1
673
+ _copy_resume_script()
674
+ _write_resume_sidecar()
675
+ settings = _load_settings()
676
+ hooks = settings.get("hooks")
677
+ if not isinstance(hooks, dict):
678
+ hooks = {}
679
+ settings["hooks"] = hooks
680
+ session_start = hooks.get("SessionStart")
681
+ if not isinstance(session_start, list):
682
+ session_start = []
683
+ hooks["SessionStart"] = session_start
684
+ session_start[:] = [e for e in (_strip_resume_hooks(e) for e in session_start) if e is not None]
685
+ session_start.append(
686
+ {"matcher": RESUME_MATCHER, "hooks": [{"type": "command", "command": _resume_command()}]}
687
+ )
688
+ _save_settings(settings)
689
+ print(_t("setup_resume_enabled", path=_resolve_resume_source()))
690
+ print(_t("setup_claude_restart_required"))
691
+ return 0
692
+
693
+
694
+ def enable_terse_mode() -> int:
695
+ setup_hook.configure_windows_utf8_output()
696
+ if not CLAUDE_SETTINGS.parent.exists():
697
+ print(_t("setup_no_agents"), file=sys.stderr)
698
+ return 1
699
+ _copy_terse_script()
700
+ _copy_terse_reminder_script()
701
+ _write_terse_sidecar()
702
+ settings = _load_settings()
703
+ hooks = settings.get("hooks")
704
+ if not isinstance(hooks, dict):
705
+ hooks = {}
706
+ settings["hooks"] = hooks
707
+ session_start = hooks.get("SessionStart")
708
+ if not isinstance(session_start, list):
709
+ session_start = []
710
+ hooks["SessionStart"] = session_start
711
+ session_start[:] = [e for e in (_strip_terse_hooks(e) for e in session_start) if e is not None]
712
+ session_start.append(
713
+ {"matcher": TERSE_MATCHER, "hooks": [{"type": "command", "command": _terse_command()}]}
714
+ )
715
+ _register_terse_reminder(settings)
716
+ _save_settings(settings)
717
+ if CODEX_CONFIG.exists():
718
+ _setup_codex_terse()
719
+ print(_t("terse_mode_enabled_msg"))
720
+ print(_t("setup_claude_restart_required"))
721
+ return 0
722
+
723
+
724
+ def disable_session_resume() -> int:
725
+ setup_hook.configure_windows_utf8_output()
726
+ if CLAUDE_SETTINGS.parent.exists():
727
+ settings = _load_settings()
728
+ session_start = _session_start_list(settings)
729
+ if session_start is not None:
730
+ kept = [e for e in (_strip_resume_hooks(e) for e in session_start) if e is not None]
731
+ if kept != session_start:
732
+ hooks = settings["hooks"]
733
+ if kept:
734
+ hooks["SessionStart"] = kept
735
+ else:
736
+ hooks.pop("SessionStart", None)
737
+ if not hooks:
738
+ settings.pop("hooks", None)
739
+ _save_settings(settings)
740
+ print(_t("setup_resume_disabled"))
741
+ for path in (RESUME_HOOK_TARGET, RESUME_PROMPT_SIDECAR):
742
+ if path.exists():
743
+ path.unlink()
744
+ return 0
745
+
746
+
747
+ def disable_terse_mode() -> int:
748
+ setup_hook.configure_windows_utf8_output()
749
+ if CLAUDE_SETTINGS.parent.exists():
750
+ settings = _load_settings()
751
+ changed = False
752
+
753
+ session_start = _session_start_list(settings)
754
+ if session_start is not None:
755
+ kept = [e for e in (_strip_terse_hooks(e) for e in session_start) if e is not None]
756
+ if kept != session_start:
757
+ hooks = settings["hooks"]
758
+ if kept:
759
+ hooks["SessionStart"] = kept
760
+ else:
761
+ hooks.pop("SessionStart", None)
762
+ changed = True
763
+
764
+ ups = _user_prompt_submit_list(settings)
765
+ if ups is not None:
766
+ kept_ups = [
767
+ e for e in (_strip_terse_reminder_hooks(e) for e in ups) if e is not None
768
+ ]
769
+ if kept_ups != ups:
770
+ hooks = settings["hooks"]
771
+ if kept_ups:
772
+ hooks["UserPromptSubmit"] = kept_ups
773
+ else:
774
+ hooks.pop("UserPromptSubmit", None)
775
+ changed = True
776
+
777
+ if changed:
778
+ hooks = settings.get("hooks")
779
+ if isinstance(hooks, dict) and not hooks:
780
+ settings.pop("hooks", None)
781
+ _save_settings(settings)
782
+ print(_t("terse_mode_disabled_msg"))
783
+ for path in (TERSE_HOOK_TARGET, TERSE_REMINDER_HOOK_TARGET, TERSE_PROMPT_SIDECAR):
784
+ if path.exists():
785
+ path.unlink()
786
+ _teardown_codex_terse()
787
+ return 0
788
+
789
+
790
+ def _installed_resume_version() -> str | None:
791
+ try:
792
+ with RESUME_HOOK_TARGET.open(encoding="utf-8") as f:
793
+ for line in f:
794
+ if line.startswith("__version__"):
795
+ return line.split("=", 1)[1].strip().strip("\"'")
796
+ except OSError:
797
+ pass
798
+ return None
799
+
800
+
801
+ def _installed_terse_version() -> str | None:
802
+ try:
803
+ with TERSE_HOOK_TARGET.open(encoding="utf-8") as f:
804
+ for line in f:
805
+ if line.startswith("__version__"):
806
+ return line.split("=", 1)[1].strip().strip("\"'")
807
+ except OSError:
808
+ pass
809
+ return None
810
+
811
+
812
+ def _self_heal_resume() -> None:
813
+ """Keep the opt-in resume hook healthy *only if already enabled* — restore a missing
814
+ script/sidecar and update a stale script. Never enables it on its own."""
815
+ if not is_resume_enabled():
816
+ return
817
+ _migrate_resume_command_if_needed()
818
+ missing = _missing_resume_artifacts()
819
+ if missing:
820
+ detail = _resume_restore_context(missing)
821
+ _copy_resume_script()
822
+ _write_resume_sidecar()
823
+ _append_self_heal_log("restore_resume_hook", detail)
824
+ elif _installed_resume_version() != RESUME_HOOK_VERSION:
825
+ old = _installed_resume_version()
826
+ _copy_resume_script()
827
+ _write_resume_sidecar()
828
+ _append_self_heal_log("update_resume_hook", f"{old or 'unknown'} -> {RESUME_HOOK_VERSION}")
829
+
830
+
831
+ def _missing_terse_artifacts() -> list[str]:
832
+ missing: list[str] = []
833
+ if not TERSE_HOOK_TARGET.exists():
834
+ missing.append("script")
835
+ if not TERSE_PROMPT_SIDECAR.exists():
836
+ missing.append("sidecar")
837
+ return missing
838
+
839
+
840
+ def _self_heal_terse_mode() -> None:
841
+ if not is_terse_mode_enabled():
842
+ return
843
+ missing = _missing_terse_artifacts()
844
+ if missing:
845
+ _copy_terse_script()
846
+ _write_terse_sidecar()
847
+ _append_self_heal_log("restore_terse_hook", f"missing={','.join(missing)}")
848
+ else:
849
+ old = _installed_terse_version()
850
+ if old != TERSE_HOOK_VERSION:
851
+ _copy_terse_script()
852
+ _write_terse_sidecar()
853
+ _append_self_heal_log(
854
+ "update_terse_hook", f"{old or 'unknown'} -> {TERSE_HOOK_VERSION}"
855
+ )
856
+ _self_heal_terse_reminder()
857
+ _self_heal_codex_terse()
858
+
859
+
860
+ def _self_heal_terse_reminder() -> None:
861
+ """Backfill the per-message reminder hook when terse mode is on but the
862
+ UserPromptSubmit entry or its script is missing — e.g. a user who enabled terse
863
+ mode before this hook existed upgrades and self-heal installs it."""
864
+ script_missing = not TERSE_REMINDER_HOOK_TARGET.exists()
865
+ entry_missing = not is_terse_reminder_enabled()
866
+ if not (script_missing or entry_missing):
867
+ return
868
+ _copy_terse_reminder_script()
869
+ settings = _load_settings()
870
+ _register_terse_reminder(settings)
871
+ _save_settings(settings)
872
+ parts: list[str] = []
873
+ if script_missing:
874
+ parts.append("script")
875
+ if entry_missing:
876
+ parts.append("entry")
877
+ _append_self_heal_log("restore_terse_reminder_hook", f"missing={','.join(parts)}")
878
+
879
+
880
+ def _migrate_resume_command_if_needed() -> None:
881
+ settings = _load_settings()
882
+ entries = _session_start_list(settings)
883
+ if not entries:
884
+ return
885
+ new_command = _resume_command()
886
+ changed = False
887
+ for entry in entries:
888
+ if not isinstance(entry, dict) or not _is_resume_entry(entry):
889
+ continue
890
+ hooks = entry.get("hooks")
891
+ if not isinstance(hooks, list):
892
+ continue
893
+ for hook in hooks:
894
+ if not isinstance(hook, dict):
895
+ continue
896
+ command = hook.get("command")
897
+ if (
898
+ not isinstance(command, str)
899
+ or not any(marker in command for marker in _RESUME_MARKERS)
900
+ or command == new_command
901
+ ):
902
+ continue
903
+ hook["command"] = new_command
904
+ changed = True
905
+ if not changed:
906
+ return
907
+ _save_settings(settings)
908
+ _append_self_heal_log(
909
+ "migrate_resume_command",
910
+ f"{_resolve_resume_source()} -> {RESUME_HOOK_TARGET}",
911
+ )
912
+
913
+
914
+ def _missing_resume_artifacts() -> list[str]:
915
+ missing: list[str] = []
916
+ if not RESUME_HOOK_TARGET.exists():
917
+ missing.append("script")
918
+ if not RESUME_PROMPT_SIDECAR.exists():
919
+ missing.append("sidecar")
920
+ return missing
921
+
922
+
923
+ def _resume_restore_context(missing: list[str]) -> str:
924
+ parts = [f"missing={','.join(missing)}"]
925
+ elapsed = _seconds_since_last_self_heal("restore_resume_hook")
926
+ if elapsed is not None:
927
+ parts.append(f"seconds_since_previous_restore={elapsed}")
928
+ command = _installed_resume_command()
929
+ if command:
930
+ source = _resolve_resume_source().as_posix()
931
+ target = RESUME_HOOK_TARGET.as_posix()
932
+ if source in command:
933
+ parts.append("registered=source")
934
+ elif target in command:
935
+ parts.append("registered=target")
936
+ else:
937
+ parts.append("registered=other")
938
+ recent = _recent_claude_dir_changes()
939
+ if recent:
940
+ parts.append(f"recent_claude_entries={recent}")
941
+ return "; ".join(parts)
942
+
943
+
944
+ def _seconds_since_last_self_heal(action: str) -> int | None:
945
+ try:
946
+ settings = _load_settings()
947
+ except SystemExit:
948
+ return None
949
+ usage_settings = settings.get(BACKUP_KEY)
950
+ if not isinstance(usage_settings, dict):
951
+ return None
952
+ log = usage_settings.get("selfHealLog")
953
+ if not isinstance(log, list):
954
+ return None
955
+ for entry in reversed(log):
956
+ if not isinstance(entry, dict) or entry.get("action") != action:
957
+ continue
958
+ timestamp = entry.get("timestamp")
959
+ if not isinstance(timestamp, str):
960
+ continue
961
+ try:
962
+ parsed = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
963
+ except ValueError:
964
+ continue
965
+ return max(0, int((datetime.now(UTC) - parsed).total_seconds()))
966
+ return None
967
+
968
+
969
+ def _installed_resume_command() -> str:
970
+ try:
971
+ settings = _load_settings()
972
+ except SystemExit:
973
+ return ""
974
+ entries = _session_start_list(settings)
975
+ if not entries:
976
+ return ""
977
+ for entry in entries:
978
+ if not isinstance(entry, dict):
979
+ continue
980
+ hooks = entry.get("hooks")
981
+ if not isinstance(hooks, list):
982
+ continue
983
+ for hook in hooks:
984
+ if not isinstance(hook, dict):
985
+ continue
986
+ command = hook.get("command")
987
+ if isinstance(command, str) and any(marker in command for marker in _RESUME_MARKERS):
988
+ return command
989
+ return ""
990
+
991
+
992
+ def _recent_claude_dir_changes(limit: int = 6) -> str:
993
+ root = CLAUDE_SETTINGS.parent
994
+ try:
995
+ entries = sorted(
996
+ (entry for entry in root.iterdir()),
997
+ key=lambda entry: entry.stat().st_mtime,
998
+ reverse=True,
999
+ )
1000
+ except OSError:
1001
+ return ""
1002
+ result: list[str] = []
1003
+ now = datetime.now(UTC).timestamp()
1004
+ for entry in entries[:limit]:
1005
+ try:
1006
+ stat_result = entry.stat()
1007
+ except OSError:
1008
+ continue
1009
+ age = max(0, int(now - stat_result.st_mtime))
1010
+ kind = "dir" if entry.is_dir() else "file"
1011
+ result.append(f"{entry.name}:{kind}:{age}s")
1012
+ return ",".join(result)
1013
+
1014
+
1015
+ def _append_self_heal_log(action: str, detail: str) -> None:
1016
+ target_dir = CLAUDE_SETTINGS.parent
1017
+ # Not usage-status.lock: that one guards the high-frequency statusline
1018
+ # writes to a different file, and sharing it would serialize them here.
1019
+ lock_file = target_dir / "usage-settings.lock"
1020
+ target_dir.mkdir(parents=True, exist_ok=True)
1021
+ lock_fd = os.open(lock_file, os.O_CREAT | os.O_RDWR, 0o600)
1022
+ try:
1023
+ with _exclusive_lock(lock_fd):
1024
+ settings = _load_settings()
1025
+ usage_settings = settings.get(BACKUP_KEY)
1026
+ if not isinstance(usage_settings, dict):
1027
+ usage_settings = {}
1028
+ settings[BACKUP_KEY] = usage_settings
1029
+ log = usage_settings.get("selfHealLog")
1030
+ if not isinstance(log, list):
1031
+ log = []
1032
+ log.append(
1033
+ {
1034
+ "timestamp": (
1035
+ datetime.now(UTC)
1036
+ .replace(microsecond=0)
1037
+ .isoformat()
1038
+ .replace("+00:00", "Z")
1039
+ ),
1040
+ "action": action,
1041
+ "detail": detail,
1042
+ }
1043
+ )
1044
+ usage_settings["selfHealLog"] = log[-20:]
1045
+ _save_settings(settings)
1046
+ finally:
1047
+ os.close(lock_fd)
1048
+
1049
+
1050
+ def _run_quietly(func: Any, *args: Any, **kwargs: Any) -> Any:
1051
+ if os.environ.get("USAGE_DEBUG") == "1":
1052
+ return func(*args, **kwargs)
1053
+ output = io.StringIO()
1054
+ with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
1055
+ return func(*args, **kwargs)
1056
+
1057
+
1058
+ def _debug_self_heal_failure(action: str, exc: BaseException) -> None:
1059
+ if os.environ.get("USAGE_DEBUG") == "1":
1060
+ print(f"usage self-heal {action} failed: {type(exc).__name__}: {exc}", file=sys.stderr)
1061
+
1062
+
1063
+ def self_heal() -> None:
1064
+ """Best-effort startup repair for usage-owned Claude statusLine hooks."""
1065
+ try:
1066
+ settings = _load_settings()
1067
+ state = _detect_current_state(settings)
1068
+ if state in {"external", "legacy-tt"}:
1069
+ return
1070
+ _migrate_bundled_python_commands_if_needed(settings)
1071
+ setup_hook._migrate_windows_statusline_command_if_needed(settings)
1072
+ if not is_setup() and "statusLine" not in settings:
1073
+ exit_code = _run_quietly(setup)
1074
+ if exit_code == 0:
1075
+ _append_self_heal_log("install_hook", "initial setup")
1076
+ return
1077
+ except BaseException as exc:
1078
+ if isinstance(exc, KeyboardInterrupt):
1079
+ raise
1080
+ _debug_self_heal_failure("install_hook", exc)
1081
+
1082
+ try:
1083
+ state = _detect_current_state()
1084
+ if state in {"external", "legacy-tt"}:
1085
+ return
1086
+ old_version = _installed_hook_version()
1087
+ if needs_update():
1088
+ _run_quietly(update_hook)
1089
+ detail = f"{old_version or 'not installed'} -> {HOOK_VERSION}"
1090
+ _append_self_heal_log("update_hook", detail)
1091
+ except BaseException as exc:
1092
+ if isinstance(exc, KeyboardInterrupt):
1093
+ raise
1094
+ _debug_self_heal_failure("update_hook", exc)
1095
+
1096
+ try:
1097
+ state = _detect_current_state()
1098
+ if state in {"external", "legacy-tt"}:
1099
+ return
1100
+ if not _statusline_command_target_exists() and state in {"us-direct", "us-forwarder"}:
1101
+ _copy_hook_script()
1102
+ _copy_forwarder_script()
1103
+ _append_self_heal_log("restore_hook_scripts", "statusLine command target missing")
1104
+ except BaseException as exc:
1105
+ if isinstance(exc, KeyboardInterrupt):
1106
+ raise
1107
+ _debug_self_heal_failure("restore_hook_scripts", exc)
1108
+
1109
+ try:
1110
+ import statusline_settings
1111
+
1112
+ if (
1113
+ setup_hook.AGY_SETTINGS.is_file()
1114
+ and statusline_settings._statusline_enabled()
1115
+ and (not setup_hook.is_agy_setup() or setup_hook._agy_hook_script_is_stale())
1116
+ and setup_hook._setup_agy()
1117
+ ):
1118
+ _append_self_heal_log("setup_agy", "installed or updated Antigravity status line")
1119
+ except BaseException as exc:
1120
+ if isinstance(exc, KeyboardInterrupt):
1121
+ raise
1122
+ _debug_self_heal_failure("setup_agy", exc)
1123
+
1124
+ try:
1125
+ result = _read_codex_config() if setup_hook.CODEX_CONFIG.is_file() else None
1126
+ if (
1127
+ result is not None
1128
+ and setup_hook._codex_status_line(result[1]) in setup_hook.LEGACY_CODEX_STATUS_LINES
1129
+ ):
1130
+ setup_hook._setup_codex()
1131
+ result = _read_codex_config()
1132
+ if (
1133
+ result is not None
1134
+ and setup_hook._codex_status_line(result[1]) == setup_hook.CODEX_STATUS_LINE
1135
+ ):
1136
+ _append_self_heal_log("setup_codex", "upgraded Codex status line segments")
1137
+ except BaseException as exc:
1138
+ if isinstance(exc, KeyboardInterrupt):
1139
+ raise
1140
+ _debug_self_heal_failure("setup_codex", exc)
1141
+
1142
+ try:
1143
+ _self_heal_resume()
1144
+ except BaseException as exc:
1145
+ if isinstance(exc, KeyboardInterrupt):
1146
+ raise
1147
+ _debug_self_heal_failure("resume_hook", exc)
1148
+
1149
+ try:
1150
+ _self_heal_terse_mode()
1151
+ except BaseException as exc:
1152
+ if isinstance(exc, KeyboardInterrupt):
1153
+ raise
1154
+ _debug_self_heal_failure("terse_hook", exc)