aiwcli 0.10.3 → 0.11.0

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 (189) 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 +104 -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/hook-utils.ts +129 -50
  24. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  25. package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
  26. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  27. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  28. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
  29. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  30. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  31. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  32. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  33. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
  34. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  35. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  36. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  37. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  38. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  39. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  40. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  41. package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
  42. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  43. package/dist/templates/cc-native/.claude/settings.json +175 -185
  44. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  45. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  46. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  47. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  70. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
  71. package/oclif.manifest.json +1 -1
  72. package/package.json +1 -1
  73. package/dist/templates/_shared/hooks/__init__.py +0 -16
  74. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  75. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  76. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  87. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  88. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  89. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  90. package/dist/templates/_shared/hooks/session_end.py +0 -173
  91. package/dist/templates/_shared/hooks/session_start.py +0 -206
  92. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  93. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  94. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  95. package/dist/templates/_shared/lib/__init__.py +0 -1
  96. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  98. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  100. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  108. package/dist/templates/_shared/lib/base/constants.py +0 -358
  109. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  110. package/dist/templates/_shared/lib/base/inference.py +0 -307
  111. package/dist/templates/_shared/lib/base/logger.py +0 -305
  112. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  113. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  114. package/dist/templates/_shared/lib/base/utils.py +0 -263
  115. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  116. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  118. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  130. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  131. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  132. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  133. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  134. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  135. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  137. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  138. package/dist/templates/_shared/lib/templates/README.md +0 -206
  139. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  140. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  142. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  145. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  146. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  149. package/dist/templates/_shared/scripts/status_line.py +0 -716
  150. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  151. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  152. package/dist/templates/cc-native/MIGRATION.md +0 -86
  153. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  154. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  160. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  161. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  162. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  163. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  164. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  165. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  173. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  174. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  175. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  176. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  185. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  186. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  187. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  189. 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()