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,339 +0,0 @@
1
- """Common utilities for hook scripts.
2
-
3
- Provides standardized boilerplate for:
4
- - Path setup for imports
5
- - JSON parsing from stdin
6
- - Hook payload validation
7
- - Error handling decorators
8
- """
9
-
10
- import json
11
- import os
12
- import sys
13
- from datetime import datetime, timezone
14
- from functools import wraps
15
- from pathlib import Path
16
- from typing import Any, Callable, Dict, Optional, TypeVar
17
-
18
- from .logger import log_hook_error, hook_log, log_debug, log_info, log_warn, log_error, log_diagnostic, set_context_path, set_session_id
19
-
20
-
21
- # Context window baseline: tokens not visible in hook data
22
- # (system prompt, tools, MCP tokens)
23
- # See: https://github.com/anthropics/claude-code/issues/13783
24
- CONTEXT_BASELINE_TOKENS = 22_600
25
- DEFAULT_CONTEXT_WINDOW_SIZE = 200_000
26
-
27
-
28
- def parse_context_window(hook_input: dict) -> tuple:
29
- """Parse context window from hook input.
30
-
31
- Returns (tokens_used, max_tokens) or (None, None).
32
- tokens_used includes baseline offset for system prompt/tools.
33
- """
34
- context_window = hook_input.get("context_window")
35
- if not context_window:
36
- return None, None
37
- current_usage = context_window.get("current_usage")
38
- if not current_usage:
39
- return None, None
40
- cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
41
- input_tokens = current_usage.get("input_tokens", 0) or 0
42
- cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
43
- output_tokens = current_usage.get("output_tokens", 0) or 0
44
- content_tokens = cache_read + input_tokens + cache_creation + output_tokens
45
- tokens_used = content_tokens + CONTEXT_BASELINE_TOKENS
46
- max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW_SIZE
47
- return tokens_used, max_tokens
48
-
49
-
50
- def get_context_percent_remaining(hook_input: dict) -> tuple:
51
- """Get context percentage remaining with context.json fallback.
52
-
53
- Tries two sources in order:
54
- 1. Hook input context_window data (most accurate, real-time)
55
- 2. context.json remaining_percentage (written by status_line.py)
56
-
57
- Returns:
58
- (percent_remaining, tokens_used, max_tokens) where tokens_used and
59
- max_tokens may be None if data came from context.json fallback.
60
- Returns (None, None, None) if no data available from either source.
61
- """
62
- # Source 1: Hook input (most accurate)
63
- tokens_used, max_tokens = parse_context_window(hook_input)
64
- if tokens_used is not None and max_tokens is not None and max_tokens > 0:
65
- remaining = max_tokens - tokens_used
66
- percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
67
- return percent_remaining, tokens_used, max_tokens
68
-
69
- # Source 2: context.json fallback (written by status_line.py)
70
- try:
71
- from .utils import project_dir
72
- from ..context.context_store import get_context_by_session_id
73
-
74
- session_id = hook_input.get("session_id")
75
- if session_id:
76
- project_root = project_dir(hook_input)
77
- context = get_context_by_session_id(session_id, project_root)
78
- if context and context.last_session:
79
- pct = context.last_session.get("context_remaining_pct")
80
- if pct is not None:
81
- return pct, None, None
82
- except Exception:
83
- pass # Fallback failed — degrade gracefully
84
-
85
- return None, None, None
86
-
87
-
88
- # Type variable for generic decorators
89
- F = TypeVar('F', bound=Callable[..., Any])
90
-
91
- # Event metadata stash — populated by load_hook_input(), read by run_hook()
92
- _last_hook_event: Optional[str] = None
93
- _last_tool_name: Optional[str] = None
94
- _last_session_id: Optional[str] = None
95
-
96
-
97
- def load_hook_input() -> Optional[Dict[str, Any]]:
98
- """
99
- Load and parse JSON from stdin.
100
-
101
- Returns:
102
- Parsed JSON dict, or None if stdin is empty or invalid JSON
103
- """
104
- global _last_hook_event, _last_tool_name, _last_session_id
105
- try:
106
- input_data = sys.stdin.read().strip()
107
- if not input_data:
108
- return None
109
- result = json.loads(input_data)
110
- if isinstance(result, dict):
111
- _last_hook_event = result.get("hook_event_name")
112
- _last_tool_name = result.get("tool_name")
113
- _last_session_id = result.get("session_id")
114
- return result
115
- except json.JSONDecodeError:
116
- return None
117
-
118
-
119
- def validate_hook_event(
120
- payload: Dict[str, Any],
121
- expected_event: str,
122
- expected_tool: Optional[str] = None
123
- ) -> bool:
124
- """
125
- Validate hook event type and optional tool name.
126
-
127
- Args:
128
- payload: Hook payload from stdin
129
- expected_event: Expected hook_event_name (e.g., "PostToolUse", "PreToolUse")
130
- expected_tool: Optional expected tool_name (e.g., "TaskCreate")
131
-
132
- Returns:
133
- True if payload matches expected event/tool, False otherwise
134
- """
135
- if payload.get("hook_event_name") != expected_event:
136
- return False
137
- if expected_tool and payload.get("tool_name") != expected_tool:
138
- return False
139
- return True
140
-
141
-
142
- def get_tool_input(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
143
- """
144
- Extract and validate tool_input from payload.
145
-
146
- Args:
147
- payload: Hook payload from stdin
148
-
149
- Returns:
150
- tool_input dict, or None if missing/invalid
151
- """
152
- tool_input = payload.get("tool_input", {})
153
- return tool_input if isinstance(tool_input, dict) else None
154
-
155
-
156
- def check_skip_persistence(payload: Dict[str, Any], hook_name: str = "hook") -> bool:
157
- """
158
- Check if persistence should be skipped based on metadata flags.
159
-
160
- Args:
161
- payload: Hook payload from stdin
162
- hook_name: Name of hook for logging
163
-
164
- Returns:
165
- True if skip_persistence flag is set, False otherwise
166
- """
167
- tool_input = get_tool_input(payload)
168
- if not tool_input:
169
- return False
170
-
171
- metadata = tool_input.get("metadata", {})
172
- if isinstance(metadata, dict) and metadata.get("skip_persistence"):
173
- log_debug(hook_name, "Skipping persistence (skip_persistence flag set)")
174
- return True
175
- return False
176
-
177
-
178
- def safe_hook_main(hook_name: str) -> Callable[[F], F]:
179
- """
180
- Decorator for hook main functions with standard error handling.
181
-
182
- Catches exceptions, logs them to stderr, and returns 0 (non-blocking).
183
-
184
- Args:
185
- hook_name: Name of hook for error messages
186
-
187
- Returns:
188
- Decorator function
189
-
190
- Example:
191
- @safe_hook_main("my_hook")
192
- def main() -> int:
193
- # ... hook logic ...
194
- return 0
195
- """
196
- def decorator(func: F) -> F:
197
- @wraps(func)
198
- def wrapper(*args, **kwargs):
199
- try:
200
- return func(*args, **kwargs)
201
- except json.JSONDecodeError as e:
202
- import traceback
203
- tb = traceback.format_exc()
204
- log_hook_error(hook_name, e, traceback_str=tb)
205
- log_error(hook_name, f"JSON decode error: {e}")
206
- return 0
207
- except Exception as e:
208
- import traceback
209
- tb = traceback.format_exc()
210
- log_hook_error(hook_name, e, traceback_str=tb)
211
- log_error(hook_name, f"Unexpected error: {e}", traceback_str=tb)
212
- return 0
213
- return wrapper # type: ignore
214
- return decorator
215
-
216
-
217
- def emit_context(additional_context: str, ensure_ascii: bool = False) -> None:
218
- """Emit hookSpecificOutput with additionalContext to stdout.
219
-
220
- Args:
221
- additional_context: Context string to inject into Claude's context
222
- ensure_ascii: If True, escape non-ASCII characters in JSON output
223
- """
224
- out = {
225
- "hookSpecificOutput": {
226
- "additionalContext": additional_context,
227
- }
228
- }
229
- print(json.dumps(out, ensure_ascii=ensure_ascii))
230
-
231
-
232
- def emit_context_and_block(
233
- additional_context: str,
234
- reason: str,
235
- ensure_ascii: bool = True,
236
- ) -> None:
237
- """Emit hookSpecificOutput that denies the tool call with context and reason.
238
-
239
- Args:
240
- additional_context: Context string to inject into Claude's context
241
- reason: Reason shown to Claude for why the tool call was denied
242
- ensure_ascii: If True, escape non-ASCII characters in JSON output
243
- """
244
- out = {
245
- "hookSpecificOutput": {
246
- "additionalContext": additional_context,
247
- "permissionDecision": "deny",
248
- "permissionDecisionReason": reason,
249
- }
250
- }
251
- print(json.dumps(out, ensure_ascii=ensure_ascii))
252
-
253
-
254
- def _detect_template(script_path: str = "") -> str:
255
- """Auto-detect template origin from the hook script path.
256
-
257
- Returns "shared", a template name (e.g., "cc-native"), or "unknown".
258
- """
259
- import re
260
- path = (script_path or (sys.argv[0] if sys.argv else "")).replace("\\", "/")
261
- if "/_shared/hooks/" in path or path.startswith("_shared/hooks/"):
262
- return "shared"
263
- match = re.search(r'_([a-z][a-z0-9-]*)/hooks/', path)
264
- if match:
265
- return match.group(1) # e.g., "cc-native"
266
- return "unknown"
267
-
268
-
269
- def run_hook(main_func: Callable[[], int], hook_name: str = "unknown") -> None:
270
- """
271
- Standard hook entry point wrapper with lifecycle logging.
272
-
273
- Logs HOOK_START before calling main, HOOK_END after completion.
274
- Catches unhandled exceptions and logs them before exiting cleanly.
275
-
276
- Args:
277
- main_func: Hook main function that returns exit code
278
- hook_name: Name of the hook for error logging
279
-
280
- Example:
281
- if __name__ == "__main__":
282
- run_hook(main, "my_hook")
283
- """
284
- import time
285
- start_time = time.monotonic()
286
- template = _detect_template()
287
- event = _last_hook_event or "unknown"
288
- tool = _last_tool_name
289
-
290
- # Wire session_id into logger so all log entries carry it
291
- if _last_session_id:
292
- set_session_id(_last_session_id)
293
-
294
- # HOOK_START
295
- start_data: Dict[str, Any] = {"lifecycle": "start", "template": template, "event": event}
296
- if tool:
297
- start_data["tool"] = tool
298
- log_info(hook_name, "HOOK_START", data=start_data)
299
-
300
- exit_code = 0
301
- status = "success"
302
- error_info = None
303
-
304
- try:
305
- result = main_func()
306
- exit_code = result if isinstance(result, int) else 0
307
- status = "blocked" if exit_code != 0 else "success"
308
- except SystemExit as e:
309
- exit_code = e.code if isinstance(e.code, int) else (1 if e.code else 0)
310
- status = "blocked" if exit_code != 0 else "success"
311
- except Exception as e:
312
- import traceback
313
- exit_code = 0 # Non-blocking
314
- status = "error"
315
- error_info = (e, traceback.format_exc())
316
-
317
- # HOOK_END
318
- duration_ms = round((time.monotonic() - start_time) * 1000, 1)
319
- end_data: Dict[str, Any] = {
320
- "lifecycle": "end", "status": status,
321
- "duration_ms": duration_ms, "exit_code": exit_code,
322
- "template": template,
323
- }
324
- end_event = _last_hook_event or event # Re-read after main() populated it
325
- end_tool = _last_tool_name or tool
326
- end_data["event"] = end_event
327
- if end_tool:
328
- end_data["tool"] = end_tool
329
- if error_info:
330
- e, tb = error_info
331
- end_data["error_type"] = type(e).__name__
332
- log_hook_error(hook_name, e, traceback_str=tb)
333
- log_error(hook_name, f"HOOK_END: {e}", data=end_data, traceback_str=tb)
334
- elif status == "blocked":
335
- log_warn(hook_name, "HOOK_END", data=end_data)
336
- else:
337
- log_info(hook_name, "HOOK_END", data=end_data)
338
-
339
- raise SystemExit(exit_code)
@@ -1,307 +0,0 @@
1
- """Inference utility for AI-powered text processing.
2
-
3
- Provides a unified interface for Claude API calls using the claude CLI.
4
- Supports multiple model tiers: fast (Haiku), standard (Sonnet), smart (Opus).
5
- """
6
- import json
7
- import re
8
- import subprocess
9
- import sys
10
- import os
11
- from typing import Optional
12
-
13
- from .logger import log_debug, log_info, log_warn, log_error
14
- from dataclasses import dataclass
15
-
16
-
17
- @dataclass
18
- class InferenceResult:
19
- """Result from an inference call."""
20
- success: bool
21
- output: str
22
- error: Optional[str] = None
23
- latency_ms: int = 0
24
-
25
-
26
- # Model configurations
27
- MODELS = {
28
- "fast": "claude-3-haiku-20240307",
29
- "standard": "claude-sonnet-4-20250514",
30
- "smart": "claude-opus-4-20250514",
31
- }
32
-
33
- TIMEOUTS = {
34
- "fast": 15, # 15 seconds
35
- "standard": 30, # 30 seconds
36
- "smart": 90, # 90 seconds
37
- }
38
-
39
-
40
- def inference(
41
- system_prompt: str,
42
- user_prompt: str,
43
- level: str = "fast",
44
- timeout: Optional[int] = None,
45
- ) -> InferenceResult:
46
- """
47
- Run inference using the claude CLI.
48
-
49
- Args:
50
- system_prompt: System instructions for the model
51
- user_prompt: User message to process
52
- level: Model level - "fast" (Haiku), "standard" (Sonnet), "smart" (Opus)
53
- timeout: Custom timeout in seconds (uses level default if not specified)
54
-
55
- Returns:
56
- InferenceResult with success status, output, and any error
57
- """
58
- import time
59
- start_time = time.time()
60
-
61
- model = MODELS.get(level, MODELS["fast"])
62
- timeout_sec = timeout or TIMEOUTS.get(level, TIMEOUTS["fast"])
63
-
64
- # Combine prompts
65
- full_prompt = f"{system_prompt}\n\n{user_prompt}"
66
-
67
- # Build command
68
- cmd = [
69
- "claude",
70
- "--model", model,
71
- "--print",
72
- "--setting-sources", "",
73
- "-p", full_prompt,
74
- ]
75
-
76
- # Remove ANTHROPIC_API_KEY to force subscription auth
77
- env = os.environ.copy()
78
- env.pop("ANTHROPIC_API_KEY", None)
79
-
80
- try:
81
- result = subprocess.run(
82
- cmd,
83
- capture_output=True,
84
- text=True,
85
- timeout=timeout_sec,
86
- env=env,
87
- # Windows needs shell=True for command resolution
88
- shell=(sys.platform == "win32"),
89
- )
90
-
91
- latency_ms = int((time.time() - start_time) * 1000)
92
-
93
- if result.returncode != 0:
94
- return InferenceResult(
95
- success=False,
96
- output=result.stdout.strip() if result.stdout else "",
97
- error=result.stderr.strip() if result.stderr else f"Exit code: {result.returncode}",
98
- latency_ms=latency_ms,
99
- )
100
-
101
- return InferenceResult(
102
- success=True,
103
- output=result.stdout.strip(),
104
- latency_ms=latency_ms,
105
- )
106
-
107
- except subprocess.TimeoutExpired:
108
- latency_ms = int((time.time() - start_time) * 1000)
109
- return InferenceResult(
110
- success=False,
111
- output="",
112
- error=f"Timeout after {timeout_sec}s",
113
- latency_ms=latency_ms,
114
- )
115
- except FileNotFoundError:
116
- latency_ms = int((time.time() - start_time) * 1000)
117
- return InferenceResult(
118
- success=False,
119
- output="",
120
- error="claude CLI not found",
121
- latency_ms=latency_ms,
122
- )
123
- except Exception as e:
124
- latency_ms = int((time.time() - start_time) * 1000)
125
- return InferenceResult(
126
- success=False,
127
- output="",
128
- error=str(e),
129
- latency_ms=latency_ms,
130
- )
131
-
132
-
133
- # Stop words for filtering (from corpus analysis of 1,424 documents)
134
- from .stop_words import STOP_WORDS
135
-
136
-
137
- def filter_stop_words(text: str) -> str:
138
- """Remove stop words from text, keeping only content keywords."""
139
- from .utils import clean_text_for_slug
140
- cleaned = clean_text_for_slug(text)
141
- words = cleaned.split()
142
- filtered = [w for w in words if w not in STOP_WORDS and len(w) > 1]
143
- return ' '.join(filtered)
144
-
145
-
146
- # System prompt for generating context ID summaries (keyword extraction for recognition)
147
- CONTEXT_ID_SYSTEM_PROMPT = """Extract 6-12 keywords from what the user wants to do.
148
-
149
- Rules:
150
- - Output 6-12 keywords only
151
- - Keywords: nouns, verbs, adjectives, technical terms, proper names
152
- - NO function words: the, to, with, for, in, a, an, of, on, is, it, and, or, that, this, be, as, at, by, from
153
- - Most important/specific words preferred
154
- - No punctuation, no quotes
155
-
156
- Examples:
157
- - "I want to add user authentication" -> "add user authentication login security JWT tokens webapp service"
158
- - "Fix the bug in the login flow" -> "fix bug login flow validation error redirect session auth handler"
159
- - "Can you help me refactor this code" -> "refactor code cleanup architecture maintainability legacy modules structure patterns"
160
- - "Update the README with new instructions" -> "update README documentation instructions setup configuration install guide steps"
161
-
162
- Output ONLY the keywords separated by spaces, nothing else."""
163
-
164
-
165
- def generate_semantic_summary(prompt: str, timeout: int = 15) -> Optional[str]:
166
- """
167
- Generate a keyword summary of a user prompt.
168
-
169
- Uses Sonnet for quality inference. Returns None if inference fails.
170
-
171
- Args:
172
- prompt: User prompt to summarize
173
- timeout: Timeout in seconds (default 15)
174
-
175
- Returns:
176
- Keyword summary string (5-10 words) or None if failed
177
- """
178
- result = inference(
179
- system_prompt=CONTEXT_ID_SYSTEM_PROMPT,
180
- user_prompt=prompt,
181
- level="standard",
182
- timeout=timeout,
183
- )
184
-
185
- if not result.success or not result.output:
186
- return None
187
-
188
- # Clean up the output
189
- summary = result.output.strip()
190
- # Remove any quotes
191
- summary = summary.strip('"\'')
192
- # Remove trailing punctuation
193
- summary = summary.rstrip('.!?')
194
-
195
- # Filter stop words
196
- summary = filter_stop_words(summary)
197
-
198
- # Validate 6-12 words for sufficient context
199
- words = summary.split()
200
- if len(words) < 6 or len(words) > 12:
201
- return None
202
-
203
- return summary
204
-
205
-
206
- # System prompt for generating context ID slugs (8-12 word summary phrases for folder names)
207
- CONTEXT_ID_SLUG_PROMPT = """You generate short title phrases for work sessions. These become folder names like `260206-1959-fix-auth-middleware-redirect-loop-session-timeout`.
208
-
209
- Users scan 100+ such names to find past sessions. Your title must make THIS session instantly recognizable.
210
-
211
- Rules:
212
- - Exactly 8-12 lowercase words
213
- - First word is an action verb (fix, add, implement, refactor, update, create, remove, optimize, debug, migrate, integrate, configure, deploy, scaffold, restructure)
214
- - Coherent phrase, not disjointed keywords — reads like a short task description
215
- - Prefer specific technical terms over generic words
216
- - No articles (the, a, an), no pronouns, no filler words, no punctuation, no quotes
217
- - Input may come from speech-to-text with filler words (uh, um, like, you know, basically, so) — ignore them entirely
218
-
219
- Examples:
220
-
221
- Input: "um so basically I need to like fix the auth bug in the login page"
222
- {"slug": "fix authentication bug login page redirect session handling flow"}
223
-
224
- Input: "hey uh can we add dark mode to the settings page"
225
- {"slug": "add dark mode toggle settings page user preference storage"}
226
-
227
- Input: "the context ids are bad can we change how we generate them towards a summary"
228
- {"slug": "improve context id generation use prompt summary slugs"}
229
-
230
- Input: "I want to refactor the database connection pooling for PostgreSQL"
231
- {"slug": "refactor postgresql database connection pooling optimize query performance"}
232
-
233
- Input: "so like you know the webhook retry logic is broken and stuff"
234
- {"slug": "fix webhook retry logic broken error handling recovery mechanism"}
235
-
236
- Input: "update the CI pipeline to cache node modules between runs"
237
- {"slug": "update ci pipeline cache node modules between workflow runs"}
238
-
239
- Respond with ONLY a JSON object: {"slug": "your 8-12 word phrase here"}"""
240
-
241
-
242
- def generate_context_id_slug(prompt: str, timeout: int = 5) -> Optional[str]:
243
- """
244
- Generate a 5-12 word context ID slug from a user prompt using AI inference.
245
-
246
- Uses Haiku (fast tier) for low-latency summary generation within hook timeout budgets.
247
- Prompts for JSON output {"slug": "..."} with fallback to raw text parsing.
248
-
249
- Args:
250
- prompt: Raw user prompt to summarize (may include STT filler words)
251
- timeout: Timeout in seconds (default 3, fits within 5-10s hook budget)
252
-
253
- Returns:
254
- Space-separated summary slug (5-12 words) or None if failed
255
- """
256
- # Truncate input to 500 chars to keep inference fast
257
- truncated = prompt[:500] if len(prompt) > 500 else prompt
258
-
259
- result = inference(
260
- system_prompt=CONTEXT_ID_SLUG_PROMPT,
261
- user_prompt=truncated,
262
- level="fast",
263
- timeout=timeout,
264
- )
265
-
266
- if not result.success or not result.output:
267
- log_warn("inference", f"Context ID slug inference failed: {result.error}")
268
- return None
269
-
270
- raw = result.output.strip()
271
-
272
- # Parse JSON response {"slug": "..."}, fall back to raw text
273
- slug = None
274
- try:
275
- parsed = json.loads(raw)
276
- if isinstance(parsed, dict) and "slug" in parsed:
277
- slug = parsed["slug"]
278
- except (json.JSONDecodeError, TypeError):
279
- pass
280
-
281
- if not slug:
282
- # Fallback: treat entire output as raw text
283
- slug = raw
284
-
285
- # Clean: strip quotes, punctuation, hyphens
286
- slug = slug.strip('"\'`')
287
- slug = slug.rstrip('.!?')
288
- slug = slug.replace('-', ' ')
289
-
290
- # Remove non-alphanumeric chars (except spaces)
291
- slug = re.sub(r'[^a-zA-Z0-9 ]', '', slug)
292
-
293
- # Normalize whitespace
294
- slug = re.sub(r'\s+', ' ', slug).strip()
295
-
296
- words = slug.split()
297
-
298
- # Validate word count: truncate if over 12, reject if under 5
299
- if len(words) > 12:
300
- words = words[:12]
301
- if len(words) < 5:
302
- log_debug("inference", f"Context ID slug too short ({len(words)} words): '{slug}'")
303
- return None
304
-
305
- result_slug = ' '.join(words)
306
- log_debug("inference", f"Generated context ID slug: '{result_slug}' ({result.latency_ms}ms)")
307
- return result_slug