aiwcli 0.10.3 → 0.11.1

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 (191) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  24. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  25. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  26. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
  27. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  28. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  29. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
  30. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  31. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  32. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  33. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  34. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
  35. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  36. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  37. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
  38. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  39. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  40. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  41. package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
  42. package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
  43. package/dist/templates/_shared/scripts/status_line.ts +687 -0
  44. package/dist/templates/cc-native/.claude/settings.json +175 -185
  45. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  47. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  48. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +2 -2
  75. package/dist/templates/_shared/hooks/__init__.py +0 -16
  76. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  87. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  88. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  89. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  90. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  91. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  92. package/dist/templates/_shared/hooks/session_end.py +0 -173
  93. package/dist/templates/_shared/hooks/session_start.py +0 -206
  94. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  95. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  96. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  97. package/dist/templates/_shared/lib/__init__.py +0 -1
  98. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  100. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  108. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  109. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  110. package/dist/templates/_shared/lib/base/constants.py +0 -358
  111. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  112. package/dist/templates/_shared/lib/base/inference.py +0 -307
  113. package/dist/templates/_shared/lib/base/logger.py +0 -305
  114. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  115. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  116. package/dist/templates/_shared/lib/base/utils.py +0 -263
  117. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  118. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  131. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  132. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  133. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  134. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  135. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  136. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  137. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  138. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  139. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  140. package/dist/templates/_shared/lib/templates/README.md +0 -206
  141. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  142. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  147. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  148. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  151. package/dist/templates/_shared/scripts/status_line.py +0 -716
  152. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  153. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  154. package/dist/templates/cc-native/MIGRATION.md +0 -86
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  160. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  161. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  162. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  163. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  164. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  165. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  166. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  174. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  175. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  176. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  187. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  188. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  189. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  190. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  191. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -1,716 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Status line for Claude Code sessions.
3
-
4
- Renders context window usage and git status with ANSI colors.
5
- Optionally persists context_window data to the session's state.json.
6
-
7
- Ported from PAI statusline.ts — context and git sections only.
8
-
9
- Usage: echo '{"session_id":"...","model":{"display_name":"Opus"},...}' | python status_line.py
10
- """
11
- import json
12
- import os
13
- import re
14
- import subprocess
15
- import sys
16
- from datetime import datetime
17
- from pathlib import Path
18
- from typing import Any, Dict, Optional, Tuple
19
-
20
- # ---------------------------------------------------------------------------
21
- # Path setup (matches save_handoff.py pattern)
22
- # ---------------------------------------------------------------------------
23
- SCRIPT_DIR = Path(__file__).resolve().parent
24
- SHARED_ROOT = SCRIPT_DIR.parent # _shared/
25
- sys.path.insert(0, str(SHARED_ROOT))
26
-
27
- from lib.base.atomic_write import atomic_write
28
- from lib.base.hook_utils import CONTEXT_BASELINE_TOKENS
29
-
30
- # Cache file for session_id → context_id mapping
31
- OUTPUT_DIR = Path(".") / "_output"
32
- STATUSLINE_CACHE = OUTPUT_DIR / ".statusline-cache.json"
33
-
34
- # ---------------------------------------------------------------------------
35
- # NO_COLOR support (https://no-color.org)
36
- # ---------------------------------------------------------------------------
37
- NO_COLOR = bool(os.environ.get("NO_COLOR"))
38
-
39
- RESET = "" if NO_COLOR else "\x1b[0m"
40
-
41
- # Structural
42
- SLATE_300 = "" if NO_COLOR else "\x1b[38;2;203;213;225m"
43
- SLATE_400 = "" if NO_COLOR else "\x1b[38;2;148;163;184m"
44
- SLATE_500 = "" if NO_COLOR else "\x1b[38;2;100;116;139m"
45
- SLATE_600 = "" if NO_COLOR else "\x1b[38;2;71;85;105m"
46
-
47
- # Semantic
48
- EMERALD = "" if NO_COLOR else "\x1b[38;2;74;222;128m"
49
- ROSE = "" if NO_COLOR else "\x1b[38;2;251;113;133m"
50
- AMBER = "" if NO_COLOR else "\x1b[38;2;251;191;36m"
51
-
52
- # Context colors
53
- CTX_PRIMARY = "" if NO_COLOR else "\x1b[38;2;129;140;248m"
54
- CTX_SECONDARY = "" if NO_COLOR else "\x1b[38;2;165;180;252m"
55
- CTX_ACCENT = "" if NO_COLOR else "\x1b[38;2;139;92;246m"
56
- CTX_BUCKET_EMPTY = "" if NO_COLOR else "\x1b[38;2;75;82;95m"
57
-
58
- # Git colors
59
- GIT_PRIMARY = "" if NO_COLOR else "\x1b[38;2;56;189;248m"
60
- GIT_VALUE = "" if NO_COLOR else "\x1b[38;2;186;230;253m"
61
- GIT_DIR = "" if NO_COLOR else "\x1b[38;2;147;197;253m"
62
- GIT_CLEAN = "" if NO_COLOR else "\x1b[38;2;125;211;252m"
63
- GIT_MODIFIED = "" if NO_COLOR else "\x1b[38;2;96;165;250m"
64
- GIT_ADDED = "" if NO_COLOR else "\x1b[38;2;59;130;246m"
65
- GIT_STASH = "" if NO_COLOR else "\x1b[38;2;165;180;252m"
66
- GIT_AGE_FRESH = "" if NO_COLOR else "\x1b[38;2;125;211;252m"
67
- GIT_AGE_RECENT = "" if NO_COLOR else "\x1b[38;2;96;165;250m"
68
- GIT_AGE_STALE = "" if NO_COLOR else "\x1b[38;2;59;130;246m"
69
- GIT_AGE_OLD = "" if NO_COLOR else "\x1b[38;2;99;102;241m"
70
-
71
- # ---------------------------------------------------------------------------
72
- # Display modes
73
- # ---------------------------------------------------------------------------
74
-
75
- def get_terminal_width() -> int:
76
- """Detect terminal width with fallbacks."""
77
- # Try COLUMNS env var first
78
- cols_env = os.environ.get("COLUMNS")
79
- if cols_env:
80
- try:
81
- cols = int(cols_env)
82
- if cols > 0:
83
- return cols
84
- except ValueError:
85
- pass
86
-
87
- # Try os.get_terminal_size
88
- try:
89
- return os.get_terminal_size().columns
90
- except (OSError, ValueError):
91
- pass
92
-
93
- return 80
94
-
95
-
96
- def get_display_mode(width: int) -> str:
97
- """Map terminal width to display mode."""
98
- if width < 35:
99
- return "nano"
100
- if width < 55:
101
- return "micro"
102
- if width < 80:
103
- return "mini"
104
- return "normal"
105
-
106
-
107
- # ---------------------------------------------------------------------------
108
- # Color helpers
109
- # ---------------------------------------------------------------------------
110
-
111
- def get_bucket_color(pos: int, max_pos: int) -> str:
112
- """Get gradient color for context bar bucket at position."""
113
- if NO_COLOR:
114
- return ""
115
- pct = (pos * 100) // max_pos
116
-
117
- if pct <= 33:
118
- r = 74 + ((250 - 74) * pct) // 33
119
- g = 222 + ((204 - 222) * pct) // 33
120
- b = 128 + ((21 - 128) * pct) // 33
121
- elif pct <= 66:
122
- t = pct - 33
123
- r = 250 + ((251 - 250) * t) // 33
124
- g = 204 + ((146 - 204) * t) // 33
125
- b = 21 + ((60 - 21) * t) // 33
126
- else:
127
- t = pct - 66
128
- r = 251 + ((239 - 251) * t) // 34
129
- g = 146 + ((68 - 146) * t) // 34
130
- b = 60 + ((68 - 60) * t) // 34
131
-
132
- return f"\x1b[38;2;{r};{g};{b}m"
133
-
134
-
135
- # ---------------------------------------------------------------------------
136
- # Context bar rendering
137
- # ---------------------------------------------------------------------------
138
-
139
- def render_context_bar(width: int, pct: int) -> Tuple[str, str]:
140
- """Render the context usage bar with gradient colors.
141
-
142
- Returns (bar_string, last_filled_color).
143
- """
144
- pct = max(0, min(100, pct))
145
- filled = (pct * width) // 100
146
- last_color = EMERALD
147
- parts = []
148
-
149
- for i in range(1, width + 1):
150
- if i <= filled:
151
- color = get_bucket_color(i, width)
152
- last_color = color
153
- parts.append(f"{color}\u26C1{RESET}")
154
- else:
155
- parts.append(f"{CTX_BUCKET_EMPTY}\u26C1{RESET}")
156
- if width > 8:
157
- parts.append(" ")
158
-
159
- return "".join(parts).rstrip(), last_color
160
-
161
-
162
- # ---------------------------------------------------------------------------
163
- # Separator
164
- # ---------------------------------------------------------------------------
165
-
166
- SEPARATOR = f"{SLATE_600}" + "\u2500" * 72 + f"{RESET}"
167
-
168
-
169
- # ---------------------------------------------------------------------------
170
- # Context section
171
- # ---------------------------------------------------------------------------
172
-
173
- def shorten_model(name: str) -> str:
174
- """Shorten common model display names."""
175
- replacements = [
176
- ("claude-opus-4-6", "opus-4.6"),
177
- ("claude-opus-4-5", "opus-4.5"),
178
- ("claude-sonnet-4", "sonnet-4"),
179
- ("claude-3-5-sonnet", "sonnet-3.5"),
180
- ("claude-3-5-haiku", "haiku-3.5"),
181
- ("claude-", ""),
182
- ]
183
- result = name
184
- for old, new in replacements:
185
- result = result.replace(old, new)
186
- return result
187
-
188
-
189
- def render_context(
190
- mode: str,
191
- context_pct: int,
192
- context_k: int,
193
- max_k: int,
194
- time_display: str,
195
- model_name: str,
196
- ) -> None:
197
- """Render the context usage section."""
198
- if context_pct <= 33:
199
- pct_color = EMERALD
200
- elif context_pct <= 66:
201
- pct_color = AMBER
202
- else:
203
- pct_color = ROSE
204
-
205
- short_model = shorten_model(model_name)
206
-
207
- if mode == "nano":
208
- bar, _ = render_context_bar(5, context_pct)
209
- print(
210
- f"{CTX_PRIMARY}\u25C9{RESET} {CTX_ACCENT}{short_model}{RESET} "
211
- f"{bar} {pct_color}{context_pct}%{RESET} "
212
- f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
213
- )
214
- elif mode == "micro":
215
- bar, _ = render_context_bar(6, context_pct)
216
- print(
217
- f"{CTX_PRIMARY}\u25C9{RESET} {CTX_ACCENT}{short_model}{RESET} "
218
- f"{SLATE_600}\u2502{RESET} "
219
- f"{bar} {pct_color}{context_pct}%{RESET} {SLATE_500}({context_k}k){RESET} "
220
- f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
221
- )
222
- elif mode == "mini":
223
- bar, _ = render_context_bar(8, context_pct)
224
- print(
225
- f"{CTX_PRIMARY}\u25C9{RESET} {CTX_ACCENT}{short_model}{RESET} "
226
- f"{SLATE_600}\u2502{RESET} "
227
- f"{CTX_SECONDARY}CTX:{RESET} {bar} "
228
- f"{pct_color}{context_pct}%{RESET} {SLATE_500}({context_k}k/{max_k}k){RESET} "
229
- f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
230
- )
231
- else: # normal
232
- bar, last_color = render_context_bar(16, context_pct)
233
- print(
234
- f"{CTX_PRIMARY}\u25C9{RESET} {CTX_SECONDARY}Model:{RESET} {CTX_ACCENT}{short_model}{RESET} "
235
- f"{SLATE_600}\u2502{RESET} "
236
- f"{CTX_SECONDARY}Context:{RESET} {bar} "
237
- f"{last_color}{context_pct}%{RESET} {SLATE_500}({context_k}k/{max_k}k){RESET} "
238
- f"{SLATE_600}\u2502{RESET} "
239
- f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
240
- )
241
-
242
- print(SEPARATOR)
243
-
244
-
245
- # ---------------------------------------------------------------------------
246
- # Git status
247
- # ---------------------------------------------------------------------------
248
-
249
- def _run_git(args: list, cwd: str, timeout: int = 2) -> Optional[str]:
250
- """Run a git command and return stdout, or None on failure."""
251
- try:
252
- kwargs: Dict[str, Any] = {
253
- "capture_output": True,
254
- "text": True,
255
- "timeout": timeout,
256
- "cwd": cwd,
257
- }
258
- if sys.platform == "win32":
259
- kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
260
- result = subprocess.run(["git"] + args, **kwargs)
261
- if result.returncode == 0:
262
- return result.stdout.strip()
263
- except Exception:
264
- pass
265
- return None
266
-
267
-
268
- def get_git_status(cwd: str) -> Optional[Dict[str, Any]]:
269
- """Gather git repository status."""
270
- # Check if git repo
271
- if _run_git(["rev-parse", "--git-dir"], cwd) is None:
272
- return None
273
-
274
- status: Dict[str, Any] = {
275
- "branch": "detached",
276
- "modified": 0,
277
- "staged": 0,
278
- "untracked": 0,
279
- "stash_count": 0,
280
- "ahead": 0,
281
- "behind": 0,
282
- "age_display": "",
283
- "age_color": GIT_AGE_FRESH,
284
- }
285
-
286
- # Branch
287
- branch = _run_git(["branch", "--show-current"], cwd)
288
- if branch:
289
- status["branch"] = branch
290
-
291
- # Modified files
292
- diff = _run_git(["diff", "--name-only"], cwd)
293
- if diff:
294
- status["modified"] = len([l for l in diff.splitlines() if l])
295
-
296
- # Staged files
297
- staged = _run_git(["diff", "--cached", "--name-only"], cwd)
298
- if staged:
299
- status["staged"] = len([l for l in staged.splitlines() if l])
300
-
301
- # Untracked files
302
- untracked = _run_git(["ls-files", "--others", "--exclude-standard"], cwd)
303
- if untracked:
304
- status["untracked"] = len([l for l in untracked.splitlines() if l])
305
-
306
- # Stash count
307
- stash = _run_git(["stash", "list"], cwd)
308
- if stash:
309
- status["stash_count"] = len([l for l in stash.splitlines() if l])
310
-
311
- # Ahead/behind
312
- ab = _run_git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], cwd)
313
- if ab:
314
- parts = ab.split()
315
- if len(parts) >= 2:
316
- status["ahead"] = int(parts[0] or 0)
317
- status["behind"] = int(parts[1] or 0)
318
-
319
- # Commit age
320
- log = _run_git(["log", "-1", "--format=%ct"], cwd)
321
- if log:
322
- try:
323
- import time
324
- last_epoch = int(log)
325
- now_epoch = int(time.time())
326
- age_sec = now_epoch - last_epoch
327
- age_min = age_sec // 60
328
- age_hrs = age_sec // 3600
329
- age_days = age_sec // 86400
330
-
331
- if age_min < 1:
332
- status["age_display"] = "now"
333
- status["age_color"] = GIT_AGE_FRESH
334
- elif age_hrs < 1:
335
- status["age_display"] = f"{age_min}m"
336
- status["age_color"] = GIT_AGE_FRESH
337
- elif age_hrs < 24:
338
- status["age_display"] = f"{age_hrs}h"
339
- status["age_color"] = GIT_AGE_RECENT
340
- elif age_days < 7:
341
- status["age_display"] = f"{age_days}d"
342
- status["age_color"] = GIT_AGE_STALE
343
- else:
344
- status["age_display"] = f"{age_days}d"
345
- status["age_color"] = GIT_AGE_OLD
346
- except (ValueError, TypeError):
347
- pass
348
-
349
- return status
350
-
351
-
352
- def render_git(mode: str, git: Dict[str, Any], dir_name: str) -> None:
353
- """Render the git status section."""
354
- total_changed = git["modified"] + git["staged"]
355
- status_icon = "*" if (total_changed > 0 or git["untracked"] > 0) else "\u2713"
356
-
357
- if mode == "nano":
358
- line = f"{GIT_PRIMARY}\u25C8{RESET} {GIT_DIR}{dir_name}{RESET} {GIT_VALUE}{git['branch']}{RESET} "
359
- if status_icon == "\u2713":
360
- line += f"{GIT_CLEAN}\u2713{RESET}"
361
- else:
362
- line += f"{GIT_MODIFIED}*{total_changed}{RESET}"
363
- print(line)
364
-
365
- elif mode == "micro":
366
- line = f"{GIT_PRIMARY}\u25C8{RESET} {GIT_DIR}{dir_name}{RESET} {GIT_VALUE}{git['branch']}{RESET}"
367
- if git["age_display"]:
368
- line += f" {git['age_color']}{git['age_display']}{RESET}"
369
- line += " "
370
- if status_icon == "\u2713":
371
- line += f"{GIT_CLEAN}{status_icon}{RESET}"
372
- else:
373
- line += f"{GIT_MODIFIED}{status_icon}{total_changed}{RESET}"
374
- print(line)
375
-
376
- elif mode == "mini":
377
- line = (
378
- f"{GIT_PRIMARY}\u25C8{RESET} {GIT_DIR}{dir_name}{RESET} "
379
- f"{SLATE_600}\u2502{RESET} {GIT_VALUE}{git['branch']}{RESET}"
380
- )
381
- if git["age_display"]:
382
- line += f" {SLATE_600}\u2502{RESET} {git['age_color']}{git['age_display']}{RESET}"
383
- line += f" {SLATE_600}\u2502{RESET} "
384
- if status_icon == "\u2713":
385
- line += f"{GIT_CLEAN}{status_icon}{RESET}"
386
- else:
387
- line += f"{GIT_MODIFIED}{status_icon}{total_changed}{RESET}"
388
- if git["untracked"] > 0:
389
- line += f" {GIT_ADDED}+{git['untracked']}{RESET}"
390
- print(line)
391
-
392
- else: # normal
393
- line = (
394
- f"{GIT_PRIMARY}\u25C8{RESET} {GIT_PRIMARY}PWD:{RESET} {GIT_DIR}{dir_name}{RESET} "
395
- f"{SLATE_600}\u2502{RESET} "
396
- f"{GIT_PRIMARY}Branch:{RESET} {GIT_VALUE}{git['branch']}{RESET}"
397
- )
398
- if git["age_display"]:
399
- line += f" {SLATE_600}\u2502{RESET} {GIT_PRIMARY}Age:{RESET} {git['age_color']}{git['age_display']}{RESET}"
400
- if git["stash_count"] > 0:
401
- line += f" {SLATE_600}\u2502{RESET} {GIT_PRIMARY}Stash:{RESET} {GIT_STASH}{git['stash_count']}{RESET}"
402
-
403
- if total_changed > 0 or git["untracked"] > 0:
404
- line += f" {SLATE_600}\u2502{RESET} "
405
- if total_changed > 0:
406
- line += f"{GIT_PRIMARY}Mod:{RESET} {GIT_MODIFIED}{total_changed}{RESET}"
407
- if git["untracked"] > 0:
408
- if total_changed > 0:
409
- line += " "
410
- line += f"{GIT_PRIMARY}New:{RESET} {GIT_ADDED}{git['untracked']}{RESET}"
411
- else:
412
- line += f" {SLATE_600}\u2502{RESET} {GIT_CLEAN}\u2713 clean{RESET}"
413
-
414
- if git["ahead"] > 0 or git["behind"] > 0:
415
- line += f" {SLATE_600}\u2502{RESET} {GIT_PRIMARY}Sync:{RESET} "
416
- if git["ahead"] > 0:
417
- line += f"{GIT_CLEAN}\u2191{git['ahead']}{RESET}"
418
- if git["behind"] > 0:
419
- line += f"{GIT_STASH}\u2193{git['behind']}{RESET}"
420
- print(line)
421
-
422
-
423
- # ---------------------------------------------------------------------------
424
- # Context manager line (line 3)
425
- # ---------------------------------------------------------------------------
426
-
427
- def render_context_manager(
428
- mode: str,
429
- context_id: str,
430
- context_state=None,
431
- ) -> None:
432
- """Render the context manager line (line 3) showing context ID, mode, and plan."""
433
- # Strip YYMMDD-HHMM- timestamp prefix from context ID for display
434
- display_id = re.sub(r"^\d{6}-\d{4}-", "", context_id)
435
- if not display_id:
436
- display_id = context_id # fallback if regex strips everything
437
-
438
- # Truncate display_id per mode
439
- max_id_len = {"nano": 14, "micro": 18, "mini": 22, "normal": 30}.get(mode, 30)
440
- truncated_id = display_id[:max_id_len]
441
- if len(display_id) > max_id_len:
442
- truncated_id += "\u2026"
443
-
444
- # Read state fields (ContextState object from context_store)
445
- state_mode = getattr(context_state, "mode", "idle") if context_state else "idle"
446
- state_plan_path = getattr(context_state, "plan_path", None) if context_state else None
447
-
448
- # Detect plan mode heuristic: if state is idle but a recent plan file exists
449
- # in ~/.claude/plans/, we're likely in active planning (transient, not persisted)
450
- active_plan_file = _find_active_plan_file()
451
- is_planning = state_mode == "idle" and active_plan_file is not None
452
-
453
- # Build mode badge
454
- mode_badge = ""
455
- if is_planning:
456
- label = "Plan" if mode == "nano" else "Planning"
457
- mode_badge = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Mode:{RESET} {AMBER}{label}{RESET}"
458
- elif state_mode == "has_plan":
459
- label = "Ready" if mode == "nano" else "Plan Ready"
460
- mode_badge = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Mode:{RESET} {EMERALD}{label}{RESET}"
461
- elif state_mode == "active":
462
- label = "Active" if mode == "nano" else "Active"
463
- mode_badge = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Mode:{RESET} {CTX_ACCENT}{label}{RESET}"
464
-
465
- # Resolve plan file path for display
466
- plan_file_path = None
467
- if is_planning:
468
- plan_file_path = active_plan_file
469
- elif state_plan_path:
470
- plan_file_path = state_plan_path
471
- elif state_mode in ("has_plan", "active"):
472
- # Fallback: check context's plans/ folder
473
- try:
474
- from lib.context.plan_manager import find_latest_plan
475
- plan_file_path = find_latest_plan(context_id)
476
- except Exception:
477
- pass
478
-
479
- # Build plan name (mini/normal only)
480
- plan_part = ""
481
- if mode in ("mini", "normal") and plan_file_path:
482
- plan_stem = re.sub(r"^\d{4}-\d{2}-\d{2}-(\d{4}-)?", "", Path(plan_file_path).stem)
483
- max_plan_len = 20 if mode == "mini" else 30
484
- truncated_plan = plan_stem[:max_plan_len]
485
- if len(plan_stem) > max_plan_len:
486
- truncated_plan += "\u2026"
487
- plan_part = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Plan:{RESET} {SLATE_300}{truncated_plan}{RESET}"
488
-
489
- if mode == "nano":
490
- print(
491
- f"{CTX_ACCENT}\u25C6{RESET} {SLATE_400}{truncated_id}{RESET}"
492
- f"{mode_badge}"
493
- )
494
- elif mode == "micro":
495
- print(
496
- f"{CTX_ACCENT}\u25C6{RESET} {SLATE_400}{truncated_id}{RESET}"
497
- f"{mode_badge}"
498
- )
499
- elif mode == "mini":
500
- print(
501
- f"{CTX_ACCENT}\u25C6{RESET} {SLATE_400}{truncated_id}{RESET}"
502
- f"{mode_badge}{plan_part}"
503
- )
504
- else: # normal
505
- print(
506
- f"{CTX_ACCENT}\u25C6{RESET} {CTX_SECONDARY}Context:{RESET} {SLATE_300}{truncated_id}{RESET}"
507
- f"{mode_badge}{plan_part}"
508
- )
509
-
510
-
511
- def render_no_context(mode: str) -> None:
512
- """Render a prominent indicator when no context is active."""
513
- warn = f"{ROSE}\u26A0 {RESET}" # ⚠ (extra space after glyph)
514
- if mode == "nano":
515
- print(f"{warn} {ROSE}NO CONTEXT{RESET}")
516
- elif mode == "micro":
517
- print(f"{warn} {ROSE}NO CONTEXT{RESET}")
518
- elif mode == "mini":
519
- print(f"{warn} {ROSE}NO CONTEXT{RESET}")
520
- else: # normal
521
- print(f"{warn} {ROSE}NO CONTEXT{RESET} {SLATE_500}\u2014 type ^ for context manager{RESET}")
522
-
523
-
524
- # ---------------------------------------------------------------------------
525
- # Context persistence
526
- # ---------------------------------------------------------------------------
527
-
528
- def _load_cache() -> Dict[str, Any]:
529
- """Load the statusline cache file."""
530
- try:
531
- if STATUSLINE_CACHE.exists():
532
- return json.loads(STATUSLINE_CACHE.read_text(encoding="utf-8"))
533
- except Exception:
534
- pass
535
- return {}
536
-
537
-
538
- def _save_cache(cache: Dict[str, Any]) -> None:
539
- """Save the statusline cache file."""
540
- try:
541
- STATUSLINE_CACHE.parent.mkdir(parents=True, exist_ok=True)
542
- STATUSLINE_CACHE.write_text(
543
- json.dumps(cache, indent=2, ensure_ascii=False), encoding="utf-8"
544
- )
545
- except Exception:
546
- pass
547
-
548
-
549
- def _resolve_context_id(session_id: str) -> Optional[str]:
550
- """Resolve session_id to context_id, using cache when possible."""
551
- if not session_id or session_id == "unknown":
552
- return None
553
-
554
- # Check cache first
555
- cache = _load_cache()
556
- cached_entry = cache.get("sessions", {}).get(session_id)
557
- if cached_entry and cached_entry.get("context_id") is not None:
558
- return cached_entry["context_id"]
559
-
560
- # Cache miss — look up via context manager
561
- try:
562
- from lib.context.context_store import get_context_by_session_id
563
- context = get_context_by_session_id(session_id)
564
- if context:
565
- # Update cache
566
- if "sessions" not in cache:
567
- cache["sessions"] = {}
568
- cache["sessions"][session_id] = {"context_id": context.id}
569
- _save_cache(cache)
570
- return context.id
571
- except Exception:
572
- pass
573
-
574
- # Mark as no-context in cache to avoid repeated lookups
575
- if "sessions" not in cache:
576
- cache["sessions"] = {}
577
- cache["sessions"][session_id] = {"context_id": None}
578
- _save_cache(cache)
579
- return None
580
-
581
-
582
- def _load_context_state(context_id: str):
583
- """Load context state from state.json (with context.json fallback)."""
584
- try:
585
- from lib.context.context_store import load_state
586
- return load_state(context_id)
587
- except Exception:
588
- return None
589
-
590
-
591
- def _find_active_plan_file() -> Optional[str]:
592
- """Find most recent plan file in ~/.claude/plans/."""
593
- try:
594
- plans_dir = Path.home() / ".claude" / "plans"
595
- if not plans_dir.exists():
596
- return None
597
- plan_files = list(plans_dir.glob("*.md"))
598
- if not plan_files:
599
- return None
600
- plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
601
- return str(plan_files[0])
602
- except Exception:
603
- return None
604
-
605
-
606
- def _write_context_window(context_id: str, context_window_data: Dict[str, Any]) -> None:
607
- """Write context_window data to state.json last_session."""
608
- try:
609
- from lib.context.context_store import get_context as get_ctx, save_state
610
- state = get_ctx(context_id)
611
- if state:
612
- if state.last_session is None:
613
- state.last_session = {}
614
- state.last_session["context_remaining_pct"] = context_window_data.get("remaining_percentage")
615
- save_state(state)
616
- except Exception:
617
- pass
618
-
619
-
620
- # ---------------------------------------------------------------------------
621
- # Main
622
- # ---------------------------------------------------------------------------
623
-
624
- def main() -> None:
625
- """Read stdin JSON, render status line, optionally persist context data."""
626
- # Force UTF-8 stdout on Windows to support Unicode symbols
627
- if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
628
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
629
-
630
- # Read JSON from stdin
631
- try:
632
- input_data = json.loads(sys.stdin.read())
633
- except Exception:
634
- input_data = {}
635
-
636
- # Terminal width and mode
637
- term_width = get_terminal_width()
638
- mode = get_display_mode(term_width)
639
-
640
- # Extract input fields
641
- session_id = input_data.get("session_id", "")
642
- model_name = (input_data.get("model") or {}).get("display_name", "unknown")
643
- cost = input_data.get("cost") or {}
644
- duration_ms = cost.get("total_duration_ms", 0)
645
- workspace = input_data.get("workspace") or {}
646
- current_dir = workspace.get("project_dir", os.getcwd())
647
- dir_name = os.path.basename(current_dir)
648
-
649
- # Context window data
650
- ctx_win = input_data.get("context_window") or {}
651
- usage = ctx_win.get("current_usage") or {}
652
- cache_read = usage.get("cache_read_input_tokens", 0)
653
- input_tokens = usage.get("input_tokens", 0)
654
- cache_creation = usage.get("cache_creation_input_tokens", 0)
655
- output_tokens = usage.get("output_tokens", 0)
656
- context_max = ctx_win.get("context_window_size", 200000)
657
-
658
- # Calculate context percentage
659
- # Use used_percentage if available (pre-calculated), else raw tokens + baseline
660
- used_pct = ctx_win.get("used_percentage")
661
- if used_pct is not None:
662
- context_pct = int(used_pct)
663
- total_input = cache_read + input_tokens + cache_creation
664
- context_used = total_input + output_tokens + CONTEXT_BASELINE_TOKENS
665
- else:
666
- total_input = cache_read + input_tokens + cache_creation
667
- context_used = total_input + output_tokens + CONTEXT_BASELINE_TOKENS
668
- context_pct = (context_used * 100) // context_max if context_max > 0 else 0
669
-
670
- context_k = context_used // 1000
671
- max_k = context_max // 1000
672
-
673
- # Format duration
674
- duration_sec = duration_ms // 1000
675
- if duration_sec >= 3600:
676
- time_display = f"{duration_sec // 3600}h{(duration_sec % 3600) // 60}m"
677
- elif duration_sec >= 60:
678
- time_display = f"{duration_sec // 60}m{duration_sec % 60}s"
679
- else:
680
- time_display = f"{duration_sec}s"
681
-
682
- # Resolve context ID for display and persistence
683
- context_id = _resolve_context_id(session_id)
684
-
685
- # Render context section
686
- render_context(mode, context_pct, context_k, max_k, time_display, model_name)
687
-
688
- # Render git section
689
- git = get_git_status(current_dir)
690
- if git:
691
- render_git(mode, git, dir_name)
692
-
693
- # Render context manager line (line 3) with separator
694
- print(SEPARATOR)
695
- if context_id:
696
- context_state = _load_context_state(context_id)
697
- render_context_manager(mode, context_id, context_state)
698
- else:
699
- render_no_context(mode)
700
-
701
- # Persist context_window to state.json
702
- if context_id:
703
- _write_context_window(context_id, {
704
- "used_percentage": context_pct,
705
- "remaining_percentage": 100 - context_pct,
706
- "context_window_size": context_max,
707
- "tokens_used": context_used,
708
- "total_input_tokens": total_input,
709
- "total_output_tokens": output_tokens,
710
- "model": model_name,
711
- "last_updated": datetime.now().isoformat(timespec="seconds"),
712
- })
713
-
714
-
715
- if __name__ == "__main__":
716
- main()