@superclaude-org/superflag 3.1.1 → 3.1.4
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.
- package/LICENSE +20 -20
- package/README.md +467 -292
- package/SUPERFLAG.md +80 -0
- package/dist/install.js +307 -110
- package/dist/install.js.map +1 -1
- package/dist/server.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/flags.yaml +1509 -270
- package/hooks/superflag.py +192 -192
- package/package.json +3 -2
package/hooks/superflag.py
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
SuperFlag - Claude Code Hook
|
|
4
|
-
Simple flag detection and message output for Claude Code
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
import json
|
|
9
|
-
import yaml
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
# Constants
|
|
13
|
-
FLAGS_YAML_PATH = Path.home() / ".superflag" / "flags.yaml"
|
|
14
|
-
AUTO_FLAG = '--auto'
|
|
15
|
-
RESET_FLAG = '--reset'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def load_config():
|
|
19
|
-
"""Load YAML configuration file"""
|
|
20
|
-
if not FLAGS_YAML_PATH.exists():
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
with open(FLAGS_YAML_PATH, 'r', encoding='utf-8') as f:
|
|
25
|
-
return yaml.safe_load(f)
|
|
26
|
-
except Exception:
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def extract_valid_flags(user_input, valid_flags):
|
|
31
|
-
"""Extract flags using simple 'in' check - 100% coverage"""
|
|
32
|
-
# Use set to avoid duplicates, then convert back to list
|
|
33
|
-
found_flags = [flag for flag in valid_flags if flag in user_input]
|
|
34
|
-
# Preserve order from valid_flags but remove duplicates
|
|
35
|
-
return list(dict.fromkeys(found_flags))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def get_auto_message(has_other_flags, other_flags, hook_messages):
|
|
39
|
-
"""Generate message for --auto flag"""
|
|
40
|
-
if has_other_flags:
|
|
41
|
-
# Auto with other flags
|
|
42
|
-
config = hook_messages.get('auto_with_context', {})
|
|
43
|
-
# Format as comma-separated list instead of JSON array
|
|
44
|
-
other_flags_str = ', '.join(other_flags)
|
|
45
|
-
return config.get('message', '').format(
|
|
46
|
-
other_flags=other_flags_str
|
|
47
|
-
)
|
|
48
|
-
else:
|
|
49
|
-
# Auto alone
|
|
50
|
-
config = hook_messages.get('auto_authority', {})
|
|
51
|
-
# No formatting needed for auto alone message
|
|
52
|
-
return config.get('message', '')
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_other_flags_message(other_flags, hook_messages):
|
|
56
|
-
"""Generate message for non-auto flags"""
|
|
57
|
-
other_flags_set = set(other_flags)
|
|
58
|
-
|
|
59
|
-
# Check if it's ONLY --reset
|
|
60
|
-
if other_flags_set == {RESET_FLAG}:
|
|
61
|
-
config = hook_messages.get('reset_protocol', {})
|
|
62
|
-
# Check if --reset is with other flags
|
|
63
|
-
elif RESET_FLAG in other_flags_set:
|
|
64
|
-
config = hook_messages.get('reset_with_others', {})
|
|
65
|
-
else:
|
|
66
|
-
# Standard execution for all other cases
|
|
67
|
-
config = hook_messages.get('standard_execution', {})
|
|
68
|
-
|
|
69
|
-
message_template = config.get('message', '')
|
|
70
|
-
if message_template:
|
|
71
|
-
# Format as comma-separated list instead of JSON array
|
|
72
|
-
return message_template.format(
|
|
73
|
-
flag_list=', '.join(other_flags),
|
|
74
|
-
flags=', '.join(other_flags)
|
|
75
|
-
)
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def generate_messages(flags, hook_messages):
|
|
80
|
-
"""Generate appropriate messages based on detected flags"""
|
|
81
|
-
if not flags:
|
|
82
|
-
return []
|
|
83
|
-
|
|
84
|
-
messages = []
|
|
85
|
-
detected_set = set(flags)
|
|
86
|
-
|
|
87
|
-
# Process --auto flag independently
|
|
88
|
-
if AUTO_FLAG in detected_set:
|
|
89
|
-
other_flags = [f for f in flags if f != AUTO_FLAG]
|
|
90
|
-
auto_message = get_auto_message(bool(other_flags), other_flags, hook_messages)
|
|
91
|
-
if auto_message:
|
|
92
|
-
messages.append(auto_message)
|
|
93
|
-
else:
|
|
94
|
-
other_flags = flags
|
|
95
|
-
|
|
96
|
-
# Process remaining flags if any (but not when --auto is present)
|
|
97
|
-
if other_flags and AUTO_FLAG not in detected_set:
|
|
98
|
-
other_message = get_other_flags_message(other_flags, hook_messages)
|
|
99
|
-
if other_message:
|
|
100
|
-
messages.append(other_message)
|
|
101
|
-
|
|
102
|
-
return messages
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def process_input(user_input):
|
|
106
|
-
"""Main processing logic"""
|
|
107
|
-
# Load configuration
|
|
108
|
-
config = load_config()
|
|
109
|
-
if not config:
|
|
110
|
-
return None
|
|
111
|
-
|
|
112
|
-
# Get valid flags from directives
|
|
113
|
-
directives = config.get('directives', {})
|
|
114
|
-
valid_flags = set(directives.keys())
|
|
115
|
-
|
|
116
|
-
# Extract valid flags directly (100% coverage approach)
|
|
117
|
-
flags = extract_valid_flags(user_input, valid_flags)
|
|
118
|
-
if not flags:
|
|
119
|
-
return None
|
|
120
|
-
|
|
121
|
-
# Generate messages
|
|
122
|
-
hook_messages = config.get('hook_messages', {})
|
|
123
|
-
# messages = generate_messages(flags, hook_messages)
|
|
124
|
-
messages = generate_messages(flags, hook_messages)
|
|
125
|
-
|
|
126
|
-
if messages:
|
|
127
|
-
return {
|
|
128
|
-
# 'flags': flags,
|
|
129
|
-
'messages': messages
|
|
130
|
-
}
|
|
131
|
-
return None
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def main():
|
|
135
|
-
"""Main entry point for Claude Code Hook"""
|
|
136
|
-
try:
|
|
137
|
-
# Read input from stdin
|
|
138
|
-
data = sys.stdin.read().strip()
|
|
139
|
-
|
|
140
|
-
# Parse input - Claude Code may send JSON
|
|
141
|
-
user_input = ""
|
|
142
|
-
if data:
|
|
143
|
-
# Try JSON parsing first (like hook_handler.py)
|
|
144
|
-
if data.startswith('{') and data.endswith('}'):
|
|
145
|
-
try:
|
|
146
|
-
parsed = json.loads(data)
|
|
147
|
-
# Extract prompt/message/input field
|
|
148
|
-
user_input = parsed.get('prompt', parsed.get('message', parsed.get('input', data)))
|
|
149
|
-
except json.JSONDecodeError:
|
|
150
|
-
user_input = data
|
|
151
|
-
else:
|
|
152
|
-
user_input = data
|
|
153
|
-
|
|
154
|
-
# Process input
|
|
155
|
-
result = process_input(user_input) if user_input else None
|
|
156
|
-
|
|
157
|
-
# Output result
|
|
158
|
-
if result and result.get('messages'):
|
|
159
|
-
# Plain text 출력용 메시지 준비 (JSON에는 포함 안 함)
|
|
160
|
-
display_message = ""
|
|
161
|
-
if isinstance(result.get('messages'), list):
|
|
162
|
-
display_message = "\n".join([m for m in result['messages'] if m])
|
|
163
|
-
else:
|
|
164
|
-
display_message = str(result.get('messages', ''))
|
|
165
|
-
|
|
166
|
-
# Plain text만 출력 (JSON 제거)
|
|
167
|
-
if display_message:
|
|
168
|
-
print(display_message)
|
|
169
|
-
|
|
170
|
-
# JSON 출력 제거 - Claude가 plain text도 파싱할 수 있음
|
|
171
|
-
# print(json.dumps(result, ensure_ascii=False))
|
|
172
|
-
else:
|
|
173
|
-
# No valid flags or messages
|
|
174
|
-
print("{}")
|
|
175
|
-
|
|
176
|
-
return 0
|
|
177
|
-
|
|
178
|
-
except KeyboardInterrupt:
|
|
179
|
-
# User interrupted with Ctrl+C
|
|
180
|
-
print("{}")
|
|
181
|
-
return 130
|
|
182
|
-
|
|
183
|
-
except Exception as e:
|
|
184
|
-
# Log error to stderr (not visible in Claude Code output)
|
|
185
|
-
print(f"Hook error: {str(e)}", file=sys.stderr)
|
|
186
|
-
# Return safe empty JSON for Claude
|
|
187
|
-
print("{}")
|
|
188
|
-
return 1
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if __name__ == "__main__":
|
|
192
|
-
sys.exit(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SuperFlag - Claude Code Hook
|
|
4
|
+
Simple flag detection and message output for Claude Code
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import yaml
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Constants
|
|
13
|
+
FLAGS_YAML_PATH = Path.home() / ".superflag" / "flags.yaml"
|
|
14
|
+
AUTO_FLAG = '--auto'
|
|
15
|
+
RESET_FLAG = '--reset'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config():
|
|
19
|
+
"""Load YAML configuration file"""
|
|
20
|
+
if not FLAGS_YAML_PATH.exists():
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(FLAGS_YAML_PATH, 'r', encoding='utf-8') as f:
|
|
25
|
+
return yaml.safe_load(f)
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_valid_flags(user_input, valid_flags):
|
|
31
|
+
"""Extract flags using simple 'in' check - 100% coverage"""
|
|
32
|
+
# Use set to avoid duplicates, then convert back to list
|
|
33
|
+
found_flags = [flag for flag in valid_flags if flag in user_input]
|
|
34
|
+
# Preserve order from valid_flags but remove duplicates
|
|
35
|
+
return list(dict.fromkeys(found_flags))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_auto_message(has_other_flags, other_flags, hook_messages):
|
|
39
|
+
"""Generate message for --auto flag"""
|
|
40
|
+
if has_other_flags:
|
|
41
|
+
# Auto with other flags
|
|
42
|
+
config = hook_messages.get('auto_with_context', {})
|
|
43
|
+
# Format as comma-separated list instead of JSON array
|
|
44
|
+
other_flags_str = ', '.join(other_flags)
|
|
45
|
+
return config.get('message', '').format(
|
|
46
|
+
other_flags=other_flags_str
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
# Auto alone
|
|
50
|
+
config = hook_messages.get('auto_authority', {})
|
|
51
|
+
# No formatting needed for auto alone message
|
|
52
|
+
return config.get('message', '')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_other_flags_message(other_flags, hook_messages):
|
|
56
|
+
"""Generate message for non-auto flags"""
|
|
57
|
+
other_flags_set = set(other_flags)
|
|
58
|
+
|
|
59
|
+
# Check if it's ONLY --reset
|
|
60
|
+
if other_flags_set == {RESET_FLAG}:
|
|
61
|
+
config = hook_messages.get('reset_protocol', {})
|
|
62
|
+
# Check if --reset is with other flags
|
|
63
|
+
elif RESET_FLAG in other_flags_set:
|
|
64
|
+
config = hook_messages.get('reset_with_others', {})
|
|
65
|
+
else:
|
|
66
|
+
# Standard execution for all other cases
|
|
67
|
+
config = hook_messages.get('standard_execution', {})
|
|
68
|
+
|
|
69
|
+
message_template = config.get('message', '')
|
|
70
|
+
if message_template:
|
|
71
|
+
# Format as comma-separated list instead of JSON array
|
|
72
|
+
return message_template.format(
|
|
73
|
+
flag_list=', '.join(other_flags),
|
|
74
|
+
flags=', '.join(other_flags)
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def generate_messages(flags, hook_messages):
|
|
80
|
+
"""Generate appropriate messages based on detected flags"""
|
|
81
|
+
if not flags:
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
messages = []
|
|
85
|
+
detected_set = set(flags)
|
|
86
|
+
|
|
87
|
+
# Process --auto flag independently
|
|
88
|
+
if AUTO_FLAG in detected_set:
|
|
89
|
+
other_flags = [f for f in flags if f != AUTO_FLAG]
|
|
90
|
+
auto_message = get_auto_message(bool(other_flags), other_flags, hook_messages)
|
|
91
|
+
if auto_message:
|
|
92
|
+
messages.append(auto_message)
|
|
93
|
+
else:
|
|
94
|
+
other_flags = flags
|
|
95
|
+
|
|
96
|
+
# Process remaining flags if any (but not when --auto is present)
|
|
97
|
+
if other_flags and AUTO_FLAG not in detected_set:
|
|
98
|
+
other_message = get_other_flags_message(other_flags, hook_messages)
|
|
99
|
+
if other_message:
|
|
100
|
+
messages.append(other_message)
|
|
101
|
+
|
|
102
|
+
return messages
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def process_input(user_input):
|
|
106
|
+
"""Main processing logic"""
|
|
107
|
+
# Load configuration
|
|
108
|
+
config = load_config()
|
|
109
|
+
if not config:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# Get valid flags from directives
|
|
113
|
+
directives = config.get('directives', {})
|
|
114
|
+
valid_flags = set(directives.keys())
|
|
115
|
+
|
|
116
|
+
# Extract valid flags directly (100% coverage approach)
|
|
117
|
+
flags = extract_valid_flags(user_input, valid_flags)
|
|
118
|
+
if not flags:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# Generate messages
|
|
122
|
+
hook_messages = config.get('hook_messages', {})
|
|
123
|
+
# messages = generate_messages(flags, hook_messages)
|
|
124
|
+
messages = generate_messages(flags, hook_messages)
|
|
125
|
+
|
|
126
|
+
if messages:
|
|
127
|
+
return {
|
|
128
|
+
# 'flags': flags,
|
|
129
|
+
'messages': messages
|
|
130
|
+
}
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
"""Main entry point for Claude Code Hook"""
|
|
136
|
+
try:
|
|
137
|
+
# Read input from stdin
|
|
138
|
+
data = sys.stdin.read().strip()
|
|
139
|
+
|
|
140
|
+
# Parse input - Claude Code may send JSON
|
|
141
|
+
user_input = ""
|
|
142
|
+
if data:
|
|
143
|
+
# Try JSON parsing first (like hook_handler.py)
|
|
144
|
+
if data.startswith('{') and data.endswith('}'):
|
|
145
|
+
try:
|
|
146
|
+
parsed = json.loads(data)
|
|
147
|
+
# Extract prompt/message/input field
|
|
148
|
+
user_input = parsed.get('prompt', parsed.get('message', parsed.get('input', data)))
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
user_input = data
|
|
151
|
+
else:
|
|
152
|
+
user_input = data
|
|
153
|
+
|
|
154
|
+
# Process input
|
|
155
|
+
result = process_input(user_input) if user_input else None
|
|
156
|
+
|
|
157
|
+
# Output result
|
|
158
|
+
if result and result.get('messages'):
|
|
159
|
+
# Plain text 출력용 메시지 준비 (JSON에는 포함 안 함)
|
|
160
|
+
display_message = ""
|
|
161
|
+
if isinstance(result.get('messages'), list):
|
|
162
|
+
display_message = "\n".join([m for m in result['messages'] if m])
|
|
163
|
+
else:
|
|
164
|
+
display_message = str(result.get('messages', ''))
|
|
165
|
+
|
|
166
|
+
# Plain text만 출력 (JSON 제거)
|
|
167
|
+
if display_message:
|
|
168
|
+
print(display_message)
|
|
169
|
+
|
|
170
|
+
# JSON 출력 제거 - Claude가 plain text도 파싱할 수 있음
|
|
171
|
+
# print(json.dumps(result, ensure_ascii=False))
|
|
172
|
+
else:
|
|
173
|
+
# No valid flags or messages
|
|
174
|
+
print("{}")
|
|
175
|
+
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
# User interrupted with Ctrl+C
|
|
180
|
+
print("{}")
|
|
181
|
+
return 130
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
# Log error to stderr (not visible in Claude Code output)
|
|
185
|
+
print(f"Hook error: {str(e)}", file=sys.stderr)
|
|
186
|
+
# Return safe empty JSON for Claude
|
|
187
|
+
print("{}")
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
sys.exit(main())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superclaude-org/superflag",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
4
4
|
"description": "SuperFlag - MCP-based flag system for AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,10 +36,11 @@
|
|
|
36
36
|
"dist",
|
|
37
37
|
"hooks",
|
|
38
38
|
"flags.yaml",
|
|
39
|
+
"SUPERFLAG.md",
|
|
39
40
|
"README.md"
|
|
40
41
|
],
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"@modelcontextprotocol/sdk": "^
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
43
44
|
"@types/inquirer": "^9.0.9",
|
|
44
45
|
"chalk": "^5.3.0",
|
|
45
46
|
"inquirer": "^12.9.6",
|