semantic-sift 0.2.1__py3-none-any.whl
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.
- semantic_sift/__init__.py +3 -0
- semantic_sift/cli.py +69 -0
- semantic_sift/hook.py +5 -0
- semantic_sift/hook_injector.py +526 -0
- semantic_sift/kernel.py +5 -0
- semantic_sift/onboarding.py +128 -0
- semantic_sift/onboarding_cli.py +57 -0
- semantic_sift/server.py +5 -0
- semantic_sift/telemetry.py +5 -0
- semantic_sift/telemetry_cli.py +83 -0
- semantic_sift/tools.py +288 -0
- semantic_sift-0.2.1.dist-info/METADATA +267 -0
- semantic_sift-0.2.1.dist-info/RECORD +21 -0
- semantic_sift-0.2.1.dist-info/WHEEL +5 -0
- semantic_sift-0.2.1.dist-info/entry_points.txt +5 -0
- semantic_sift-0.2.1.dist-info/licenses/LICENSE.md +201 -0
- semantic_sift-0.2.1.dist-info/top_level.txt +5 -0
- server.py +39 -0
- sift_hook.py +349 -0
- sift_kernel.py +366 -0
- telemetry_core.py +466 -0
semantic_sift/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Luis Kobayashi. All rights reserved.
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from semantic_sift import kernel
|
|
10
|
+
|
|
11
|
+
# We use stderr for logging so it doesn't corrupt the stdout data stream!
|
|
12
|
+
logging.basicConfig(stream=sys.stderr, level=logging.INFO, format="[Sift-CLI] %(message)s")
|
|
13
|
+
logger = logging.getLogger("semantic_sift_cli")
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser(description="Semantic-Sift Universal CLI (Hybrid Engine)")
|
|
17
|
+
parser.add_argument("type", choices=["logs", "semantic", "doc", "extraction", "auto"], default="auto", nargs="?", help="Type of distillation")
|
|
18
|
+
parser.add_argument("--rate", type=float, default=0.5, help="Compression rate for semantic tasks")
|
|
19
|
+
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
# 1. Read from standard input
|
|
23
|
+
input_data = sys.stdin.read()
|
|
24
|
+
if not input_data:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
char_count = len(input_data)
|
|
28
|
+
|
|
29
|
+
# 2. Hybrid Engine Routing Decision
|
|
30
|
+
if args.type == "logs" or (args.type == "auto" and char_count < 1000):
|
|
31
|
+
# FAST PATH: Heuristics or small files
|
|
32
|
+
# Ideally, we shell out to `sift-core logs` here if available.
|
|
33
|
+
# For now, we use the Python kernel equivalent.
|
|
34
|
+
logger.info(f"Routing {char_count} chars to Heuristic Engine.")
|
|
35
|
+
result = kernel.apply_heuristic_sieve(input_data)
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
# NEURAL PATH: Semantic compression
|
|
39
|
+
if char_count > 30000:
|
|
40
|
+
logger.info(f"Massive payload ({char_count} chars). Routing to PyTorch Engine (Flash Attention).")
|
|
41
|
+
# In the future, this explicitly loads PyTorch.
|
|
42
|
+
# Currently, the python kernel uses PyTorch (llmlingua).
|
|
43
|
+
result = kernel.perform_semantic_sift(input_data, rate=args.rate)
|
|
44
|
+
else:
|
|
45
|
+
logger.info(f"Standard payload ({char_count} chars). Routing to Rust/ONNX Engine.")
|
|
46
|
+
# Shell out to the Rust sidecar for low-latency ONNX execution
|
|
47
|
+
try:
|
|
48
|
+
process = subprocess.Popen(
|
|
49
|
+
["sift-core", "semantic", "--rate", str(args.rate)],
|
|
50
|
+
stdin=subprocess.PIPE,
|
|
51
|
+
stdout=subprocess.PIPE,
|
|
52
|
+
stderr=subprocess.PIPE,
|
|
53
|
+
text=True
|
|
54
|
+
)
|
|
55
|
+
stdout, stderr = process.communicate(input=input_data)
|
|
56
|
+
if process.returncode == 0:
|
|
57
|
+
result = stdout
|
|
58
|
+
else:
|
|
59
|
+
logger.warning(f"Rust engine failed: {stderr}. Falling back to PyTorch.")
|
|
60
|
+
result = kernel.perform_semantic_sift(input_data, rate=args.rate)
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
logger.info("Rust engine not found. Falling back to PyTorch.")
|
|
63
|
+
result = kernel.perform_semantic_sift(input_data, rate=args.rate)
|
|
64
|
+
|
|
65
|
+
# 3. Output to standard output
|
|
66
|
+
sys.stdout.write(result)
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
semantic_sift/hook.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Luis Kobayashi. All rights reserved.
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_runtime_hook_command() -> tuple[str, str, str]:
|
|
10
|
+
python_exe = os.path.abspath(sys.executable)
|
|
11
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
12
|
+
hook_script = os.path.abspath(os.path.join(repo_root, "sift_hook.py"))
|
|
13
|
+
if not os.path.exists(hook_script):
|
|
14
|
+
raise RuntimeError(
|
|
15
|
+
f"Semantic-Sift startup failed: hook script not found at '{hook_script}'. "
|
|
16
|
+
"Ensure sift_hook.py is present next to server.py."
|
|
17
|
+
)
|
|
18
|
+
cmd_str = f'"{python_exe}" "{hook_script}"'
|
|
19
|
+
return python_exe, hook_script, cmd_str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_windsurf_gateway_command() -> str:
|
|
23
|
+
if sys.platform == "win32":
|
|
24
|
+
return (
|
|
25
|
+
'pwsh -NoProfile -Command "$p=$env:WINDSURF_TOOL_ARGS; '
|
|
26
|
+
'if (Test-Path $p) { '
|
|
27
|
+
'if ((Get-Item $p).Length -gt 1024) { '
|
|
28
|
+
'[Console]::Error.WriteLine(\"[BLOCKED by Semantic-Sift] File > 1KB. Use sift_read_file instead.\"); '
|
|
29
|
+
'exit 2 } }"'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
'SIZE=$(stat -c %s "$WINDSURF_TOOL_ARGS" 2>/dev/null || stat -f %z "$WINDSURF_TOOL_ARGS" 2>/dev/null || wc -c < "$WINDSURF_TOOL_ARGS" 2>/dev/null); '
|
|
34
|
+
'if [ "$SIZE" -gt 1024 ] 2>/dev/null; then '
|
|
35
|
+
'echo "[BLOCKED by Semantic-Sift] File > 1KB. Use sift_read_file instead." > /dev/stderr; '
|
|
36
|
+
'exit 2; fi'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def discover_agent_configs(target_dir: str) -> list[str]:
|
|
41
|
+
found_paths = []
|
|
42
|
+
agent_dirs = [".codex/agents", ".cursor/agents", ".junie/agents", ".agents"]
|
|
43
|
+
|
|
44
|
+
for d in agent_dirs:
|
|
45
|
+
full_dir = os.path.join(target_dir, d)
|
|
46
|
+
if os.path.exists(full_dir):
|
|
47
|
+
for f in os.listdir(full_dir):
|
|
48
|
+
if f.endswith((".toml", ".md")):
|
|
49
|
+
found_paths.append(os.path.join(full_dir, f))
|
|
50
|
+
|
|
51
|
+
for root, _, files in os.walk(target_dir):
|
|
52
|
+
depth = root[len(target_dir):].count(os.sep)
|
|
53
|
+
if depth > 3:
|
|
54
|
+
continue
|
|
55
|
+
if "AGENTS.md" in files and root != target_dir:
|
|
56
|
+
found_paths.append(os.path.join(root, "AGENTS.md"))
|
|
57
|
+
|
|
58
|
+
return found_paths
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def update_toml_config(path: str, section_id: str, content: str) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
64
|
+
file_content = f.read()
|
|
65
|
+
|
|
66
|
+
block_id = f"# SIFT_SECTION_START:{section_id}"
|
|
67
|
+
block_end = f"# SIFT_SECTION_END:{section_id}"
|
|
68
|
+
full_payload = f"\n{block_id}\n# ---\n# {content}\n{block_end}\n"
|
|
69
|
+
|
|
70
|
+
pattern = re.compile(rf'{re.escape(block_id)}.*?{re.escape(block_end)}', re.DOTALL)
|
|
71
|
+
if pattern.search(file_content):
|
|
72
|
+
new_content = pattern.sub(full_payload.strip(), file_content)
|
|
73
|
+
else:
|
|
74
|
+
if "instructions =" in file_content:
|
|
75
|
+
new_content = file_content.replace("instructions = \"\"\"", f"instructions = \"\"\"\n{content}\n")
|
|
76
|
+
else:
|
|
77
|
+
new_content = file_content + full_payload
|
|
78
|
+
|
|
79
|
+
with open(path, "w", encoding="utf-8", errors="replace") as f:
|
|
80
|
+
f.write(new_content)
|
|
81
|
+
return True
|
|
82
|
+
except (OSError, re.error):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def merge_hook_json(path: str, hook_key: str, new_hook: dict, version: int | None = None) -> bool:
|
|
87
|
+
data: dict = {"hooks": {}}
|
|
88
|
+
if version:
|
|
89
|
+
data["version"] = version
|
|
90
|
+
if os.path.exists(path):
|
|
91
|
+
try:
|
|
92
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
93
|
+
data = json.load(f)
|
|
94
|
+
except (OSError, json.JSONDecodeError):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
if "hooks" not in data:
|
|
98
|
+
data["hooks"] = {}
|
|
99
|
+
hooks_list = data["hooks"].get(hook_key, [])
|
|
100
|
+
|
|
101
|
+
exists = any(h.get("command") == new_hook.get("command") for h in hooks_list)
|
|
102
|
+
if not exists:
|
|
103
|
+
data["hooks"][hook_key] = [new_hook] + hooks_list
|
|
104
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
105
|
+
json.dump(data, f, indent=2)
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def update_instruction_files(
|
|
111
|
+
section_id: str,
|
|
112
|
+
header: str,
|
|
113
|
+
content: str,
|
|
114
|
+
instruction_targets: list[str],
|
|
115
|
+
runtime_python_exe: str,
|
|
116
|
+
runtime_hook_script: str,
|
|
117
|
+
runtime_hook_command: str,
|
|
118
|
+
target_dir: str | None = None,
|
|
119
|
+
environment: str | None = None,
|
|
120
|
+
) -> list[str]:
|
|
121
|
+
actions = []
|
|
122
|
+
cwd = target_dir if target_dir else os.getcwd()
|
|
123
|
+
python_exe = runtime_python_exe
|
|
124
|
+
hook_script = runtime_hook_script
|
|
125
|
+
cmd_str = runtime_hook_command
|
|
126
|
+
block_id = f"<!-- SIFT_SECTION_START:{section_id} -->"
|
|
127
|
+
block_end = f"<!-- SIFT_SECTION_END:{section_id} -->"
|
|
128
|
+
full_payload = f"\n{block_id}\n---\n\n{header}\n{content}\n{block_end}\n"
|
|
129
|
+
|
|
130
|
+
env_lower = environment.lower() if environment else ""
|
|
131
|
+
|
|
132
|
+
targets = instruction_targets[:]
|
|
133
|
+
subagent_paths = discover_agent_configs(cwd)
|
|
134
|
+
targets.extend(subagent_paths)
|
|
135
|
+
|
|
136
|
+
for target in targets:
|
|
137
|
+
target_path = target if os.path.isabs(target) else os.path.join(cwd, target)
|
|
138
|
+
if os.path.exists(target_path):
|
|
139
|
+
try:
|
|
140
|
+
filename = os.path.basename(target_path)
|
|
141
|
+
|
|
142
|
+
if target_path.endswith(".toml"):
|
|
143
|
+
if update_toml_config(target_path, section_id, content):
|
|
144
|
+
actions.append(f"Shielded subagent config: `{filename}`.")
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if not filename.endswith((".md", ".clinerules", ".cursorrules", ".windsurfrules")):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
with open(target_path, "r", encoding="utf-8", errors="replace") as f:
|
|
151
|
+
file_content = f.read()
|
|
152
|
+
|
|
153
|
+
if any(x in file_content.lower() for x in ["always use view_file", "read the full file", "read entire file"]):
|
|
154
|
+
actions.append(f"⚠️ WARNING: Found potentially contradictory 'read full file' instructions in `{filename}`. The Sift Mandate override has been appended.")
|
|
155
|
+
|
|
156
|
+
pattern = re.compile(rf'{re.escape(block_id)}.*?{re.escape(block_end)}', re.DOTALL)
|
|
157
|
+
if pattern.search(file_content):
|
|
158
|
+
new_content = pattern.sub(full_payload.strip(), file_content)
|
|
159
|
+
with open(target_path, "w", encoding="utf-8", errors="replace") as f:
|
|
160
|
+
f.write(new_content)
|
|
161
|
+
actions.append(f"Updated `{filename}`.")
|
|
162
|
+
else:
|
|
163
|
+
with open(target_path, "a", encoding="utf-8", errors="replace") as f:
|
|
164
|
+
f.write(full_payload)
|
|
165
|
+
actions.append(f"Injected into `{filename}`.")
|
|
166
|
+
except (OSError, re.error) as e:
|
|
167
|
+
actions.append(f"Error updating `{target_path}`: {str(e)}")
|
|
168
|
+
|
|
169
|
+
if "cursor" in env_lower:
|
|
170
|
+
cursor_path = os.path.join(cwd, ".cursor", "hooks.json")
|
|
171
|
+
os.makedirs(os.path.dirname(cursor_path), exist_ok=True)
|
|
172
|
+
|
|
173
|
+
if os.path.exists(cursor_path):
|
|
174
|
+
try:
|
|
175
|
+
with open(cursor_path, "r", encoding="utf-8") as f:
|
|
176
|
+
cursor_data = json.load(f)
|
|
177
|
+
if "hooks" in cursor_data and "beforeMCPExecution" in cursor_data["hooks"]:
|
|
178
|
+
actions.append("🚨 ALERT: `beforeMCPExecution` security gateway detected in Cursor hooks. You MUST whitelist `sift_read_file` and `sift_analyze_file` or they will be blocked.")
|
|
179
|
+
except (OSError, json.JSONDecodeError):
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
if merge_hook_json(cursor_path, "postToolUse", {"command": cmd_str}, version=1):
|
|
183
|
+
actions.append("Merged into Cursor hooks.")
|
|
184
|
+
|
|
185
|
+
if "vscode" in env_lower or "github" in env_lower:
|
|
186
|
+
vscode_path = os.path.join(cwd, ".github", "hooks", "semantic-sift.json")
|
|
187
|
+
os.makedirs(os.path.dirname(vscode_path), exist_ok=True)
|
|
188
|
+
if merge_hook_json(vscode_path, "PostToolUse", {"type": "command", "command": cmd_str}):
|
|
189
|
+
actions.append("Merged into VS Code hooks.")
|
|
190
|
+
|
|
191
|
+
if "gemini" in env_lower:
|
|
192
|
+
gemini_commands_dir = os.path.join(cwd, ".gemini", "commands")
|
|
193
|
+
os.makedirs(gemini_commands_dir, exist_ok=True)
|
|
194
|
+
gemini_command_path = os.path.join(gemini_commands_dir, "sift-stats.toml")
|
|
195
|
+
|
|
196
|
+
gemini_command_content = """description = "View Semantic-Sift token savings and telemetry dashboard"
|
|
197
|
+
prompt = \"\"\"
|
|
198
|
+
!{semantic-sift-stats}
|
|
199
|
+
\"\"\"
|
|
200
|
+
"""
|
|
201
|
+
if not os.path.exists(gemini_command_path):
|
|
202
|
+
try:
|
|
203
|
+
with open(gemini_command_path, "w", encoding="utf-8") as f:
|
|
204
|
+
f.write(gemini_command_content)
|
|
205
|
+
actions.append("Injected `/sift-stats` custom command into Gemini CLI.")
|
|
206
|
+
except OSError as e:
|
|
207
|
+
actions.append(f"Error configuring Gemini CLI command: {str(e)}")
|
|
208
|
+
|
|
209
|
+
if "opencode" in env_lower:
|
|
210
|
+
opencode_plugin_path = os.path.join(cwd, ".opencode", "plugins", "semantic-sift.ts")
|
|
211
|
+
os.makedirs(os.path.dirname(opencode_plugin_path), exist_ok=True)
|
|
212
|
+
plugin_content = f"""/**
|
|
213
|
+
* Semantic-Sift Native OpenCode Plugin
|
|
214
|
+
*/
|
|
215
|
+
export const SemanticSiftPlugin = async ({{ $ }}) => {{
|
|
216
|
+
return {{
|
|
217
|
+
hooks: {{
|
|
218
|
+
"tool.execute.after": async (input, output) => {{
|
|
219
|
+
const rawContent = output.result;
|
|
220
|
+
if (typeof rawContent !== 'string' || rawContent.length < 500) return;
|
|
221
|
+
if (rawContent.includes("--- [Semantic-Sift: Native Execution] ---")) return;
|
|
222
|
+
try {{
|
|
223
|
+
const pythonExe = "{python_exe}";
|
|
224
|
+
const siftScript = "{hook_script}";
|
|
225
|
+
const payload = {{ hook_event_name: "AfterTool", tool_name: input.tool, tool_args: input.args, tool_response: {{ llmContent: rawContent }} }};
|
|
226
|
+
const response = await $`${{pythonExe}} ${{siftScript}}`.input(JSON.stringify(payload)).text();
|
|
227
|
+
const siftedData = JSON.parse(response);
|
|
228
|
+
if (siftedData?.tool_response?.llmContent) output.result = siftedData.tool_response.llmContent;
|
|
229
|
+
}} catch (error) {{ console.error("[Semantic-Sift Plugin] failed:", error); }}
|
|
230
|
+
}}
|
|
231
|
+
}}
|
|
232
|
+
}};
|
|
233
|
+
}};
|
|
234
|
+
export default SemanticSiftPlugin;
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
if not os.path.exists(opencode_plugin_path):
|
|
238
|
+
with open(opencode_plugin_path, "w", encoding="utf-8") as f:
|
|
239
|
+
f.write(plugin_content)
|
|
240
|
+
actions.append("Configured OpenCode native plugin.")
|
|
241
|
+
except OSError as e:
|
|
242
|
+
actions.append(f"Error configuring OpenCode plugin: {str(e)}")
|
|
243
|
+
|
|
244
|
+
opencode_config_path = os.path.join(cwd, "opencode.json")
|
|
245
|
+
if os.path.exists(opencode_config_path):
|
|
246
|
+
try:
|
|
247
|
+
with open(opencode_config_path, "r", encoding="utf-8") as f:
|
|
248
|
+
opencode_config = json.load(f)
|
|
249
|
+
|
|
250
|
+
if "commands" not in opencode_config:
|
|
251
|
+
opencode_config["commands"] = {}
|
|
252
|
+
|
|
253
|
+
if "/sift-onboard" not in opencode_config["commands"]:
|
|
254
|
+
opencode_config["commands"]["/sift-onboard"] = {
|
|
255
|
+
"description": "Initialize Semantic-Sift in this project",
|
|
256
|
+
"action": "run_mcp_tool",
|
|
257
|
+
"server": "semantic-sift",
|
|
258
|
+
"tool": "sift_onboard",
|
|
259
|
+
"args": {"environment": "OpenCode"}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if "/sift-stats" not in opencode_config["commands"]:
|
|
263
|
+
opencode_config["commands"]["/sift-stats"] = {
|
|
264
|
+
"description": "View Semantic-Sift token savings and telemetry dashboard",
|
|
265
|
+
"action": "run_mcp_tool",
|
|
266
|
+
"server": "semantic-sift",
|
|
267
|
+
"tool": "get_sift_stats",
|
|
268
|
+
"args": {"scope": "all"}
|
|
269
|
+
}
|
|
270
|
+
with open(opencode_config_path, "w", encoding="utf-8") as f:
|
|
271
|
+
json.dump(opencode_config, f, indent=2)
|
|
272
|
+
actions.append("Injected `/sift-stats` command into opencode.json.")
|
|
273
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
274
|
+
actions.append(f"Error updating opencode.json commands: {str(e)}")
|
|
275
|
+
|
|
276
|
+
if "claude" in env_lower:
|
|
277
|
+
claude_paths = [
|
|
278
|
+
os.path.join(os.path.expanduser("~"), ".claude", "settings.json"),
|
|
279
|
+
os.path.join(cwd, ".claude", "settings.json"),
|
|
280
|
+
]
|
|
281
|
+
for c_path in claude_paths:
|
|
282
|
+
if os.path.exists(c_path):
|
|
283
|
+
try:
|
|
284
|
+
with open(c_path, "r", encoding="utf-8") as f:
|
|
285
|
+
c_data = json.load(f)
|
|
286
|
+
except (OSError, json.JSONDecodeError):
|
|
287
|
+
c_data = {}
|
|
288
|
+
|
|
289
|
+
if "hooks" not in c_data:
|
|
290
|
+
c_data["hooks"] = {}
|
|
291
|
+
if "PostToolUse" not in c_data["hooks"]:
|
|
292
|
+
c_data["hooks"]["PostToolUse"] = []
|
|
293
|
+
|
|
294
|
+
exists = False
|
|
295
|
+
for pt_hook in c_data["hooks"]["PostToolUse"]:
|
|
296
|
+
if pt_hook.get("matcher") == "mcp__.*__.*":
|
|
297
|
+
for inner_hook in pt_hook.get("hooks", []):
|
|
298
|
+
if inner_hook.get("command") == cmd_str:
|
|
299
|
+
exists = True
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
if not exists:
|
|
303
|
+
claude_hook = {
|
|
304
|
+
"matcher": "mcp__.*__.*",
|
|
305
|
+
"hooks": [{"type": "command", "command": cmd_str}],
|
|
306
|
+
}
|
|
307
|
+
c_data["hooks"]["PostToolUse"] = [claude_hook] + c_data["hooks"]["PostToolUse"]
|
|
308
|
+
try:
|
|
309
|
+
with open(c_path, "w", encoding="utf-8") as f:
|
|
310
|
+
json.dump(c_data, f, indent=2)
|
|
311
|
+
actions.append(f"Merged into Claude Code hooks at {c_path}.")
|
|
312
|
+
except OSError as e:
|
|
313
|
+
actions.append(f"Failed to merge Claude Code hooks: {e}")
|
|
314
|
+
|
|
315
|
+
if "qwen" in env_lower:
|
|
316
|
+
qwen_paths = [
|
|
317
|
+
os.path.join(os.path.expanduser("~"), ".qwen", "settings.json"),
|
|
318
|
+
os.path.join(cwd, ".qwen", "settings.json"),
|
|
319
|
+
]
|
|
320
|
+
for q_path in qwen_paths:
|
|
321
|
+
if os.path.exists(q_path):
|
|
322
|
+
try:
|
|
323
|
+
with open(q_path, "r", encoding="utf-8") as f:
|
|
324
|
+
q_data = json.load(f)
|
|
325
|
+
except (OSError, json.JSONDecodeError):
|
|
326
|
+
q_data = {}
|
|
327
|
+
|
|
328
|
+
if "hooks" not in q_data:
|
|
329
|
+
q_data["hooks"] = {}
|
|
330
|
+
if "PostToolUse" not in q_data["hooks"]:
|
|
331
|
+
q_data["hooks"]["PostToolUse"] = []
|
|
332
|
+
|
|
333
|
+
exists = False
|
|
334
|
+
for pt_hook in q_data["hooks"]["PostToolUse"]:
|
|
335
|
+
if pt_hook.get("matcher") == "mcp__.*__.*":
|
|
336
|
+
for inner_hook in pt_hook.get("hooks", []):
|
|
337
|
+
if inner_hook.get("command") == cmd_str:
|
|
338
|
+
exists = True
|
|
339
|
+
break
|
|
340
|
+
if not exists:
|
|
341
|
+
qwen_hook = {
|
|
342
|
+
"matcher": "mcp__.*__.*",
|
|
343
|
+
"hooks": [{"type": "command", "command": cmd_str}],
|
|
344
|
+
}
|
|
345
|
+
q_data["hooks"]["PostToolUse"] = [qwen_hook] + q_data["hooks"]["PostToolUse"]
|
|
346
|
+
try:
|
|
347
|
+
with open(q_path, "w", encoding="utf-8") as f:
|
|
348
|
+
json.dump(q_data, f, indent=2)
|
|
349
|
+
actions.append(f"Merged into Qwen CLI hooks at {q_path}.")
|
|
350
|
+
except OSError as e:
|
|
351
|
+
actions.append(f"Failed to merge Qwen CLI hooks: {e}")
|
|
352
|
+
|
|
353
|
+
if "windsurf" in env_lower:
|
|
354
|
+
windsurf_paths = [
|
|
355
|
+
os.path.join(os.path.expanduser("~"), ".codeium", "windsurf", "hooks.json"),
|
|
356
|
+
os.path.join(cwd, ".windsurf", "hooks.json"),
|
|
357
|
+
]
|
|
358
|
+
for w_path in windsurf_paths:
|
|
359
|
+
if os.path.exists(w_path):
|
|
360
|
+
try:
|
|
361
|
+
with open(w_path, "r", encoding="utf-8") as f:
|
|
362
|
+
w_data = json.load(f)
|
|
363
|
+
except (OSError, json.JSONDecodeError):
|
|
364
|
+
w_data = {}
|
|
365
|
+
if "pre_mcp_tool_use" not in w_data:
|
|
366
|
+
w_data["pre_mcp_tool_use"] = []
|
|
367
|
+
|
|
368
|
+
gateway_cmd = get_windsurf_gateway_command()
|
|
369
|
+
|
|
370
|
+
exists = any(h.get("command") == gateway_cmd for h in w_data["pre_mcp_tool_use"])
|
|
371
|
+
if not exists:
|
|
372
|
+
w_data["pre_mcp_tool_use"].insert(
|
|
373
|
+
0,
|
|
374
|
+
{
|
|
375
|
+
"matcher": "mcp__.*__(read_file|view_file)",
|
|
376
|
+
"type": "command",
|
|
377
|
+
"command": gateway_cmd,
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
try:
|
|
381
|
+
with open(w_path, "w", encoding="utf-8") as f:
|
|
382
|
+
json.dump(w_data, f, indent=2)
|
|
383
|
+
actions.append(f"Injected Security Gateway into Windsurf hooks at {w_path}.")
|
|
384
|
+
except OSError as e:
|
|
385
|
+
actions.append(f"Failed to merge Windsurf hooks: {e}")
|
|
386
|
+
|
|
387
|
+
if "openclaw" in env_lower:
|
|
388
|
+
openclaw_plugin_path = os.path.join(cwd, ".openclaw", "plugins", "semantic-sift.ts")
|
|
389
|
+
os.makedirs(os.path.dirname(openclaw_plugin_path), exist_ok=True)
|
|
390
|
+
openclaw_plugin_content = f"""/**
|
|
391
|
+
* Semantic-Sift Native OpenClaw Plugin
|
|
392
|
+
*/
|
|
393
|
+
export default function (api) {{
|
|
394
|
+
api.on("tool:after", async (event, ctx) => {{
|
|
395
|
+
const rawContent = ctx.result;
|
|
396
|
+
if (typeof rawContent !== 'string' || rawContent.length < 500) return;
|
|
397
|
+
if (rawContent.includes("--- [Semantic-Sift: Native Execution] ---")) return;
|
|
398
|
+
try {{
|
|
399
|
+
const pythonExe = "{python_exe}";
|
|
400
|
+
const siftScript = "{hook_script}";
|
|
401
|
+
const payload = {{ hook_event_name: "AfterTool", tool_name: ctx.toolName, tool_response: {{ llmContent: rawContent }} }};
|
|
402
|
+
|
|
403
|
+
// Execute Python interceptor
|
|
404
|
+
const {{ execSync }} = require('child_process');
|
|
405
|
+
const response = execSync(`${{pythonExe}} ${{siftScript}}`, {{ input: JSON.stringify(payload), encoding: 'utf-8' }});
|
|
406
|
+
|
|
407
|
+
const siftedData = JSON.parse(response);
|
|
408
|
+
if (siftedData?.tool_response?.llmContent) {{
|
|
409
|
+
ctx.result = siftedData.tool_response.llmContent;
|
|
410
|
+
}}
|
|
411
|
+
}} catch (error) {{ console.error("[Semantic-Sift Plugin] failed:", error); }}
|
|
412
|
+
}});
|
|
413
|
+
}};
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
if not os.path.exists(openclaw_plugin_path):
|
|
417
|
+
with open(openclaw_plugin_path, "w", encoding="utf-8") as f:
|
|
418
|
+
f.write(openclaw_plugin_content)
|
|
419
|
+
actions.append("Configured OpenClaw native plugin.")
|
|
420
|
+
except OSError as e:
|
|
421
|
+
actions.append(f"Error configuring OpenClaw plugin: {str(e)}")
|
|
422
|
+
|
|
423
|
+
if "kilocode" in env_lower:
|
|
424
|
+
kilo_rule_dir = os.path.join(cwd, ".kilocode", "rules")
|
|
425
|
+
os.makedirs(kilo_rule_dir, exist_ok=True)
|
|
426
|
+
kilo_rule_path = os.path.join(kilo_rule_dir, "context.md")
|
|
427
|
+
if not os.path.exists(kilo_rule_path):
|
|
428
|
+
try:
|
|
429
|
+
with open(kilo_rule_path, "w", encoding="utf-8") as f:
|
|
430
|
+
f.write(f"# Semantic-Sift Kilo Code Constraints\n\n{content}")
|
|
431
|
+
actions.append("Injected Kilo Code workspace rules.")
|
|
432
|
+
except OSError as e:
|
|
433
|
+
actions.append(f"Error configuring Kilo Code rules: {str(e)}")
|
|
434
|
+
|
|
435
|
+
if "cline" in env_lower or "roo" in env_lower:
|
|
436
|
+
cline_hooks_dir = os.path.join(cwd, ".clinerules", "hooks")
|
|
437
|
+
os.makedirs(cline_hooks_dir, exist_ok=True)
|
|
438
|
+
|
|
439
|
+
cline_ps1_path = os.path.join(cline_hooks_dir, "PreToolUse.ps1")
|
|
440
|
+
cline_ps1_content = """$inputJson = $input | ConvertFrom-Json
|
|
441
|
+
if ($inputJson.preToolUse.toolName -eq 'read_file' -or $inputJson.preToolUse.toolName -eq 'view_file') {
|
|
442
|
+
$filePath = $inputJson.preToolUse.parameters.path
|
|
443
|
+
if (Test-Path $filePath) {
|
|
444
|
+
$size = (Get-Item $filePath).Length
|
|
445
|
+
if ($size -gt 1024) {
|
|
446
|
+
$response = @{ cancel = $true; errorMessage = "[BLOCKED by Semantic-Sift] File > 1KB. Use sift_read_file instead." }
|
|
447
|
+
$response | ConvertTo-Json -Compress | Write-Output
|
|
448
|
+
exit 0
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
$response = @{ cancel = $false }
|
|
453
|
+
$response | ConvertTo-Json -Compress | Write-Output
|
|
454
|
+
"""
|
|
455
|
+
if not os.path.exists(cline_ps1_path):
|
|
456
|
+
try:
|
|
457
|
+
with open(cline_ps1_path, "w", encoding="utf-8") as f:
|
|
458
|
+
f.write(cline_ps1_content)
|
|
459
|
+
actions.append("Injected Cline PreToolUse.ps1 hook.")
|
|
460
|
+
except OSError as e:
|
|
461
|
+
actions.append(f"Error configuring Cline PS1 hook: {str(e)}")
|
|
462
|
+
|
|
463
|
+
cline_bash_path = os.path.join(cline_hooks_dir, "PreToolUse")
|
|
464
|
+
cline_bash_content = """#!/bin/bash
|
|
465
|
+
INPUT=$(cat)
|
|
466
|
+
TOOL_NAME=$(echo "$INPUT" | grep -oP '(?<="toolName":")[^"]*')
|
|
467
|
+
if [[ "$TOOL_NAME" == "read_file" ]] || [[ "$TOOL_NAME" == "view_file" ]]; then
|
|
468
|
+
FILE_PATH=$(echo "$INPUT" | grep -oP '(?<="path":")[^"]*')
|
|
469
|
+
if [[ -f "$FILE_PATH" ]]; then
|
|
470
|
+
SIZE=$(wc -c < "$FILE_PATH" 2>/dev/null || stat -f %s "$FILE_PATH" 2>/dev/null || stat -c %s "$FILE_PATH" 2>/dev/null)
|
|
471
|
+
if [[ "$SIZE" -gt 1024 ]]; then
|
|
472
|
+
echo '{"cancel": true, "errorMessage": "[BLOCKED by Semantic-Sift] File > 1KB. Use sift_read_file instead."}'
|
|
473
|
+
exit 0
|
|
474
|
+
fi
|
|
475
|
+
fi
|
|
476
|
+
fi
|
|
477
|
+
echo '{"cancel": false}'
|
|
478
|
+
"""
|
|
479
|
+
if not os.path.exists(cline_bash_path):
|
|
480
|
+
try:
|
|
481
|
+
with open(cline_bash_path, "w", encoding="utf-8", newline="\n") as f:
|
|
482
|
+
f.write(cline_bash_content)
|
|
483
|
+
os.chmod(cline_bash_path, 0o755)
|
|
484
|
+
actions.append("Injected Cline PreToolUse bash hook.")
|
|
485
|
+
except OSError as e:
|
|
486
|
+
actions.append(f"Error configuring Cline bash hook: {str(e)}")
|
|
487
|
+
|
|
488
|
+
if "codex" in env_lower:
|
|
489
|
+
codex_paths = [
|
|
490
|
+
os.path.join(os.path.expanduser("~"), ".codex", "settings.json"),
|
|
491
|
+
os.path.join(cwd, ".codex", "settings.json"),
|
|
492
|
+
]
|
|
493
|
+
for co_path in codex_paths:
|
|
494
|
+
if os.path.exists(co_path):
|
|
495
|
+
try:
|
|
496
|
+
with open(co_path, "r", encoding="utf-8") as f:
|
|
497
|
+
co_data = json.load(f)
|
|
498
|
+
except (OSError, json.JSONDecodeError):
|
|
499
|
+
co_data = {}
|
|
500
|
+
|
|
501
|
+
if "hooks" not in co_data:
|
|
502
|
+
co_data["hooks"] = {}
|
|
503
|
+
if "PostToolUse" not in co_data["hooks"]:
|
|
504
|
+
co_data["hooks"]["PostToolUse"] = []
|
|
505
|
+
|
|
506
|
+
exists = False
|
|
507
|
+
for pt_hook in co_data["hooks"]["PostToolUse"]:
|
|
508
|
+
if pt_hook.get("matcher") == "mcp__.*__.*":
|
|
509
|
+
for inner_hook in pt_hook.get("hooks", []):
|
|
510
|
+
if inner_hook.get("command") == cmd_str:
|
|
511
|
+
exists = True
|
|
512
|
+
break
|
|
513
|
+
if not exists:
|
|
514
|
+
codex_hook = {
|
|
515
|
+
"matcher": "mcp__.*__.*",
|
|
516
|
+
"hooks": [{"type": "command", "command": cmd_str}],
|
|
517
|
+
}
|
|
518
|
+
co_data["hooks"]["PostToolUse"] = [codex_hook] + co_data["hooks"]["PostToolUse"]
|
|
519
|
+
try:
|
|
520
|
+
with open(co_path, "w", encoding="utf-8") as f:
|
|
521
|
+
json.dump(co_data, f, indent=2)
|
|
522
|
+
actions.append(f"Merged into Codex CLI hooks at {co_path}.")
|
|
523
|
+
except OSError as e:
|
|
524
|
+
actions.append(f"Failed to merge Codex CLI hooks: {e}")
|
|
525
|
+
|
|
526
|
+
return actions
|