Coverage for src/moai_adk/templates/.claude/hooks/moai/shared/core/__init__.py: 44.74%

38 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-19 06:05 +0900

1#!/usr/bin/env python3 

2"""Core module for Alfred Hooks 

3 

4Common type definitions and utility functions 

5""" 

6 

7from dataclasses import dataclass, field 

8from typing import Any, Literal, NotRequired, TypedDict 

9 

10 

11class HookPayload(TypedDict): 

12 """Claude Code Hook event payload type definition 

13 

14 Data structure that Claude Code passes to the Hook script. 

15 Use NotRequired because fields may vary depending on the event. 

16 """ 

17 

18 cwd: str 

19 userPrompt: NotRequired[str] # Includes only UserPromptSubmit events 

20 tool: NotRequired[str] # PreToolUse/PostToolUse events 

21 arguments: NotRequired[dict[str, Any]] # Tool arguments 

22 

23 

24@dataclass 

25class HookResult: 

26 """Hook execution result following Claude Code standard schema. 

27 

28 Attributes conform to Claude Code Hook output specification: 

29 https://docs.claude.com/en/docs/claude-code/hooks 

30 

31 Standard Fields (Claude Code schema - included in JSON output): 

32 continue_execution: Allow execution to continue (default True) 

33 suppress_output: Suppress hook output display (default False) 

34 decision: "approve" or "block" operation (optional) 

35 reason: Explanation for decision (optional) 

36 permission_decision: "allow", "deny", or "ask" (optional) 

37 system_message: Message displayed to user (top-level field) 

38 

39 Internal Fields (MoAI-ADK only - NOT in JSON output): 

40 context_files: List of context files to load (internal use only) 

41 suggestions: Suggestions for user (internal use only) 

42 exit_code: Exit code for diagnostics (internal use only) 

43 

44 Note: 

45 - systemMessage appears at TOP LEVEL in JSON output 

46 - hookSpecificOutput is ONLY used for UserPromptSubmit events 

47 - Internal fields are used for Python logic but not serialized to JSON 

48 """ 

49 

50 # Claude Code standard fields 

51 continue_execution: bool = True 

52 suppress_output: bool = False 

53 decision: Literal["approve", "block"] | None = None 

54 reason: str | None = None 

55 permission_decision: Literal["allow", "deny", "ask"] | None = None 

56 

57 # MoAI-ADK custom fields (wrapped in hookSpecificOutput) 

58 system_message: str | None = None 

59 context_files: list[str] = field(default_factory=list) 

60 suggestions: list[str] = field(default_factory=list) 

61 exit_code: int = 0 

62 

63 def to_dict(self) -> dict[str, Any]: 

64 """Convert to Claude Code standard Hook output schema. 

65 

66 Returns: 

67 Dictionary conforming to Claude Code Hook specification with: 

68 - Top-level fields: continue, suppressOutput, decision, reason, 

69 permissionDecision, systemMessage 

70 - MoAI-ADK internal fields (context_files, suggestions, exit_code) 

71 are NOT included in JSON output (used for internal logic only) 

72 

73 Examples: 

74 >>> result = HookResult(continue_execution=True) 

75 >>> result.to_dict() 

76 {'continue': True} 

77 

78 >>> result = HookResult(decision="block", reason="Dangerous") 

79 >>> result.to_dict() 

80 {'decision': 'block', 'reason': 'Dangerous'} 

81 

82 >>> result = HookResult(system_message="Test") 

83 >>> result.to_dict() 

84 {'continue': True, 'systemMessage': 'Test'} 

85 

86 Note: 

87 - systemMessage is a TOP-LEVEL field (not nested in hookSpecificOutput) 

88 - hookSpecificOutput is ONLY used for UserPromptSubmit events 

89 - context_files, suggestions, exit_code are internal-only fields 

90 """ 

91 output: dict[str, Any] = {} 

92 

93 # Add decision or continue flag 

94 if self.decision: 

95 output["decision"] = self.decision 

96 else: 

97 output["continue"] = self.continue_execution 

98 

99 # Add reason if provided (works with both decision and permissionDecision) 

100 if self.reason: 

101 output["reason"] = self.reason 

102 

103 # Add suppressOutput if True 

104 if self.suppress_output: 

105 output["suppressOutput"] = True 

106 

107 # Add permissionDecision if set 

108 if self.permission_decision: 

109 output["permissionDecision"] = self.permission_decision 

110 

111 # Add systemMessage at TOP LEVEL (required by Claude Code schema) 

112 if self.system_message: 

113 output["systemMessage"] = self.system_message 

114 

115 # Note: context_files, suggestions, exit_code are internal-only fields 

116 # and are NOT included in the JSON output per Claude Code schema 

117 

118 return output 

119 

120 def to_user_prompt_submit_dict(self) -> dict[str, Any]: 

121 """UserPromptSubmit Hook-specific output format. 

122 

123 Claude Code requires a special schema for UserPromptSubmit events. 

124 The result is wrapped in the standard Hook schema with hookSpecificOutput. 

125 

126 Returns: 

127 Claude Code UserPromptSubmit Hook Dictionary matching schema: 

128 { 

129 "continue": true, 

130 "hookSpecificOutput": { 

131 "hookEventName": "UserPromptSubmit", 

132 "additionalContext": "string" 

133 } 

134 } 

135 

136 Examples: 

137 >>> result = HookResult(context_files=["tests/"]) 

138 >>> result.to_user_prompt_submit_dict() 

139 {'continue': True, 'hookSpecificOutput': \ 

140{'hookEventName': 'UserPromptSubmit', 'additionalContext': '📎 Context: tests/'}} 

141 """ 

142 # Convert context_files to additionalContext string 

143 if self.context_files: 

144 context_str = "\n".join([f"📎 Context: {f}" for f in self.context_files]) 

145 else: 

146 context_str = "" 

147 

148 # Add system_message if there is one 

149 if self.system_message: 

150 if context_str: 

151 context_str = f"{self.system_message}\n\n{context_str}" 

152 else: 

153 context_str = self.system_message 

154 

155 return { 

156 "continue": self.continue_execution, 

157 "hookSpecificOutput": { 

158 "hookEventName": "UserPromptSubmit", 

159 "additionalContext": context_str, 

160 }, 

161 } 

162 

163 

164__all__ = ["HookPayload", "HookResult"] 

165 

166# Note: core module exports: 

167# - HookPayload, HookResult (type definitions) 

168# - project.py: detect_language, get_git_info, count_specs, get_project_language 

169# - context.py: get_jit_context 

170# - checkpoint.py: detect_risky_operation, create_checkpoint, log_checkpoint, list_checkpoints