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
« 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
4Common type definitions and utility functions
5"""
7from dataclasses import dataclass, field
8from typing import Any, Literal, NotRequired, TypedDict
11class HookPayload(TypedDict):
12 """Claude Code Hook event payload type definition
14 Data structure that Claude Code passes to the Hook script.
15 Use NotRequired because fields may vary depending on the event.
16 """
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
24@dataclass
25class HookResult:
26 """Hook execution result following Claude Code standard schema.
28 Attributes conform to Claude Code Hook output specification:
29 https://docs.claude.com/en/docs/claude-code/hooks
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)
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)
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 """
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
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
63 def to_dict(self) -> dict[str, Any]:
64 """Convert to Claude Code standard Hook output schema.
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)
73 Examples:
74 >>> result = HookResult(continue_execution=True)
75 >>> result.to_dict()
76 {'continue': True}
78 >>> result = HookResult(decision="block", reason="Dangerous")
79 >>> result.to_dict()
80 {'decision': 'block', 'reason': 'Dangerous'}
82 >>> result = HookResult(system_message="Test")
83 >>> result.to_dict()
84 {'continue': True, 'systemMessage': 'Test'}
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] = {}
93 # Add decision or continue flag
94 if self.decision:
95 output["decision"] = self.decision
96 else:
97 output["continue"] = self.continue_execution
99 # Add reason if provided (works with both decision and permissionDecision)
100 if self.reason:
101 output["reason"] = self.reason
103 # Add suppressOutput if True
104 if self.suppress_output:
105 output["suppressOutput"] = True
107 # Add permissionDecision if set
108 if self.permission_decision:
109 output["permissionDecision"] = self.permission_decision
111 # Add systemMessage at TOP LEVEL (required by Claude Code schema)
112 if self.system_message:
113 output["systemMessage"] = self.system_message
115 # Note: context_files, suggestions, exit_code are internal-only fields
116 # and are NOT included in the JSON output per Claude Code schema
118 return output
120 def to_user_prompt_submit_dict(self) -> dict[str, Any]:
121 """UserPromptSubmit Hook-specific output format.
123 Claude Code requires a special schema for UserPromptSubmit events.
124 The result is wrapped in the standard Hook schema with hookSpecificOutput.
126 Returns:
127 Claude Code UserPromptSubmit Hook Dictionary matching schema:
128 {
129 "continue": true,
130 "hookSpecificOutput": {
131 "hookEventName": "UserPromptSubmit",
132 "additionalContext": "string"
133 }
134 }
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 = ""
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
155 return {
156 "continue": self.continue_execution,
157 "hookSpecificOutput": {
158 "hookEventName": "UserPromptSubmit",
159 "additionalContext": context_str,
160 },
161 }
164__all__ = ["HookPayload", "HookResult"]
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