mcp-context-pipe 0.1.0__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.
- context_pipe/__init__.py +2 -0
- context_pipe/onboarding.py +357 -0
- context_pipe/orchestrator.py +162 -0
- context_pipe/platforms.py +112 -0
- context_pipe/server.py +171 -0
- context_pipe/skills.py +46 -0
- context_pipe/telemetry.py +166 -0
- context_pipe/wrapper.py +113 -0
- mcp_context_pipe-0.1.0.dist-info/METADATA +170 -0
- mcp_context_pipe-0.1.0.dist-info/RECORD +14 -0
- mcp_context_pipe-0.1.0.dist-info/WHEEL +5 -0
- mcp_context_pipe-0.1.0.dist-info/entry_points.txt +4 -0
- mcp_context_pipe-0.1.0.dist-info/licenses/LICENSE.md +190 -0
- mcp_context_pipe-0.1.0.dist-info/top_level.txt +1 -0
context_pipe/__init__.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Luis Kobayashi. All rights reserved.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
def build_runtime_hook_command() -> str:
|
|
11
|
+
"""Builds the absolute command string to invoke the context-pipe wrapper."""
|
|
12
|
+
python_exe = os.path.abspath(sys.executable)
|
|
13
|
+
# We use 'python -m context_pipe.orchestrator wrap' for reliability
|
|
14
|
+
return f'"{python_exe}" -m context_pipe.orchestrator wrap'
|
|
15
|
+
|
|
16
|
+
def get_security_gateway_command() -> str:
|
|
17
|
+
"""Generates a proactive inhibitor command to block large native file reads."""
|
|
18
|
+
if sys.platform == "win32":
|
|
19
|
+
return (
|
|
20
|
+
'pwsh -NoProfile -Command "$p=$env:WINDSURF_TOOL_ARGS; '
|
|
21
|
+
'if (Test-Path $p) { '
|
|
22
|
+
'if ((Get-Item $p).Length -gt 1024) { '
|
|
23
|
+
'[Console]::Error.WriteLine(\"[BLOCKED by Context-Pipe] File > 1KB. Use pipe_read_file instead.\"); '
|
|
24
|
+
'exit 2 } }"'
|
|
25
|
+
)
|
|
26
|
+
return (
|
|
27
|
+
'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); '
|
|
28
|
+
'if [ "$SIZE" -gt 1024 ] 2>/dev/null; then '
|
|
29
|
+
'echo "[BLOCKED by Context-Pipe] File > 1KB. Use pipe_read_file instead." > /dev/stderr; '
|
|
30
|
+
'exit 2; fi'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def discover_agent_configs(target_dir: str) -> List[str]:
|
|
34
|
+
"""Recursively discovers specialized agent configurations and mandates."""
|
|
35
|
+
found_paths = []
|
|
36
|
+
agent_dirs = [".codex/agents", ".cursor/agents", ".junie/agents", ".agents"]
|
|
37
|
+
|
|
38
|
+
for d in agent_dirs:
|
|
39
|
+
full_dir = os.path.join(target_dir, d)
|
|
40
|
+
if os.path.exists(full_dir):
|
|
41
|
+
for f in os.listdir(full_dir):
|
|
42
|
+
if f.endswith((".toml", ".md")):
|
|
43
|
+
found_paths.append(os.path.join(full_dir, f))
|
|
44
|
+
|
|
45
|
+
for root, _, files in os.walk(target_dir):
|
|
46
|
+
depth = root[len(target_dir):].count(os.sep)
|
|
47
|
+
if depth > 3:
|
|
48
|
+
continue
|
|
49
|
+
if "AGENTS.md" in files and root != target_dir:
|
|
50
|
+
found_paths.append(os.path.join(root, "AGENTS.md"))
|
|
51
|
+
|
|
52
|
+
return found_paths
|
|
53
|
+
|
|
54
|
+
def merge_hook_json(path: str, hook_key: str, new_hook: dict, version: int | None = None) -> bool:
|
|
55
|
+
"""Safely merges a new hook into a JSON configuration file."""
|
|
56
|
+
data: dict = {"hooks": {}}
|
|
57
|
+
if version:
|
|
58
|
+
data["version"] = version
|
|
59
|
+
|
|
60
|
+
if os.path.exists(path):
|
|
61
|
+
try:
|
|
62
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
except (OSError, json.JSONDecodeError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if "hooks" not in data:
|
|
68
|
+
data["hooks"] = {}
|
|
69
|
+
hooks_list = data["hooks"].get(hook_key, [])
|
|
70
|
+
|
|
71
|
+
# Prevent duplicates
|
|
72
|
+
exists = any(h.get("command") == new_hook.get("command") for h in hooks_list)
|
|
73
|
+
if not exists:
|
|
74
|
+
data["hooks"][hook_key] = [new_hook] + hooks_list
|
|
75
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
76
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(data, f, indent=2)
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def inject_mandates(target_dir: str, subagents: List[str]) -> List[str]:
|
|
82
|
+
"""Injects the Path-Native mandate into global and subagent instruction files."""
|
|
83
|
+
actions = []
|
|
84
|
+
block_id = "<!-- CPP_SECTION_START:mandate -->"
|
|
85
|
+
block_end = "<!-- CPP_SECTION_END:mandate -->"
|
|
86
|
+
|
|
87
|
+
mandate_text = """# Context-Pipe Mandate
|
|
88
|
+
**MANDATORY**: NEVER use native `view_file` or `read_file` tools. You MUST exclusively use `pipe_read_file(path)` to read ANY file. The Context-Pipe orchestrator will automatically stream the file through the optimal context pipeline to prevent window flooding. For large architectural searches, use `pipe_analyze_file(path)` first to determine the best approach."""
|
|
89
|
+
|
|
90
|
+
full_payload = f"\n{block_id}\n{mandate_text}\n{block_end}\n"
|
|
91
|
+
|
|
92
|
+
# Global targets
|
|
93
|
+
targets = [
|
|
94
|
+
os.path.join(target_dir, "AGENTS.md"),
|
|
95
|
+
os.path.join(target_dir, "GEMINI.md"),
|
|
96
|
+
os.path.join(target_dir, ".clinerules"),
|
|
97
|
+
os.path.join(target_dir, ".cursorrules"),
|
|
98
|
+
os.path.join(target_dir, ".windsurfrules"),
|
|
99
|
+
os.path.join(target_dir, ".github", "copilot-instructions.md")
|
|
100
|
+
]
|
|
101
|
+
targets.extend(subagents)
|
|
102
|
+
|
|
103
|
+
for target in set(targets):
|
|
104
|
+
if not os.path.exists(target):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
|
109
|
+
content = f.read()
|
|
110
|
+
|
|
111
|
+
pattern = re.compile(rf'{re.escape(block_id)}.*?{re.escape(block_end)}', re.DOTALL)
|
|
112
|
+
if pattern.search(content):
|
|
113
|
+
new_content = pattern.sub(full_payload.strip(), content)
|
|
114
|
+
with open(target, "w", encoding="utf-8") as f:
|
|
115
|
+
f.write(new_content)
|
|
116
|
+
actions.append(f"Updated mandate in `{os.path.basename(target)}`.")
|
|
117
|
+
else:
|
|
118
|
+
with open(target, "a", encoding="utf-8") as f:
|
|
119
|
+
f.write(full_payload)
|
|
120
|
+
actions.append(f"Injected mandate into `{os.path.basename(target)}`.")
|
|
121
|
+
except OSError as e:
|
|
122
|
+
actions.append(f"Error updating `{target}`: {str(e)}")
|
|
123
|
+
|
|
124
|
+
return actions
|
|
125
|
+
|
|
126
|
+
def inject_hooks(target_dir: str, environment: str) -> List[str]:
|
|
127
|
+
"""Automates the injection of Context-Pipe hooks into various IDEs/CLIs."""
|
|
128
|
+
actions = []
|
|
129
|
+
cmd_str = build_runtime_hook_command()
|
|
130
|
+
env_lower = environment.lower()
|
|
131
|
+
|
|
132
|
+
# 0. Discovery & Mandates
|
|
133
|
+
subagents = discover_agent_configs(target_dir)
|
|
134
|
+
if subagents:
|
|
135
|
+
actions.append(f"Discovered {len(subagents)} specialized subagents.")
|
|
136
|
+
|
|
137
|
+
mandate_actions = inject_mandates(target_dir, subagents)
|
|
138
|
+
actions.extend(mandate_actions)
|
|
139
|
+
|
|
140
|
+
# 1. Cursor Injection
|
|
141
|
+
if "cursor" in env_lower:
|
|
142
|
+
cursor_path = os.path.join(target_dir, ".cursor", "hooks.json")
|
|
143
|
+
if merge_hook_json(cursor_path, "postToolUse", {"command": cmd_str}, version=1):
|
|
144
|
+
actions.append("Injected Context-Pipe into Cursor hooks.")
|
|
145
|
+
|
|
146
|
+
# 2. VS Code / GitHub Injection
|
|
147
|
+
if "vscode" in env_lower or "github" in env_lower:
|
|
148
|
+
vscode_path = os.path.join(target_dir, ".github", "hooks", "context-pipe.json")
|
|
149
|
+
if merge_hook_json(vscode_path, "PostToolUse", {"type": "command", "command": cmd_str}):
|
|
150
|
+
actions.append("Injected Context-Pipe into VS Code/GitHub hooks.")
|
|
151
|
+
|
|
152
|
+
# 3. Gemini CLI Injection
|
|
153
|
+
if "gemini" in env_lower:
|
|
154
|
+
gemini_dir = os.path.join(target_dir, ".gemini", "commands")
|
|
155
|
+
os.makedirs(gemini_dir, exist_ok=True)
|
|
156
|
+
stats_cmd = """description = "View Context-Pipe ROI Balance Sheet"
|
|
157
|
+
prompt = \"\"\"
|
|
158
|
+
!{context-pipe get_pipe_stats}
|
|
159
|
+
\"\"\"
|
|
160
|
+
"""
|
|
161
|
+
with open(os.path.join(gemini_dir, "pipe-stats.toml"), "w") as f:
|
|
162
|
+
f.write(stats_cmd)
|
|
163
|
+
actions.append("Added /pipe-stats command to Gemini CLI.")
|
|
164
|
+
|
|
165
|
+
# 4. OpenCode Injection
|
|
166
|
+
if "opencode" in env_lower:
|
|
167
|
+
oc_path = os.path.join(target_dir, "opencode.json")
|
|
168
|
+
if os.path.exists(oc_path):
|
|
169
|
+
try:
|
|
170
|
+
with open(oc_path, "r") as f:
|
|
171
|
+
oc_data = json.load(f)
|
|
172
|
+
|
|
173
|
+
# 4.1 Update MCP entry
|
|
174
|
+
if "mcp" not in oc_data:
|
|
175
|
+
oc_data["mcp"] = {}
|
|
176
|
+
oc_data["mcp"]["context-pipe"] = {
|
|
177
|
+
"type": "local",
|
|
178
|
+
"command": [os.path.abspath(sys.executable), "-m", "context_pipe.server"],
|
|
179
|
+
"environment": {
|
|
180
|
+
"PIPE_CONFIG_PATH": os.path.abspath(os.path.join(target_dir, "pipes.json"))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# 4.2 Update Commands
|
|
185
|
+
if "commands" not in oc_data:
|
|
186
|
+
oc_data["commands"] = {}
|
|
187
|
+
oc_data["commands"]["/pipe-stats"] = {
|
|
188
|
+
"description": "View Context-Pipe ROI",
|
|
189
|
+
"action": "run_mcp_tool",
|
|
190
|
+
"server": "context-pipe",
|
|
191
|
+
"tool": "get_pipe_stats"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
with open(oc_path, "w") as f:
|
|
195
|
+
json.dump(oc_data, f, indent=2)
|
|
196
|
+
actions.append("Updated Context-Pipe MCP and /pipe-stats in opencode.json.")
|
|
197
|
+
|
|
198
|
+
# 4.3 Native Plugin
|
|
199
|
+
oc_plugin_dir = os.path.join(target_dir, ".opencode", "plugins")
|
|
200
|
+
os.makedirs(oc_plugin_dir, exist_ok=True)
|
|
201
|
+
oc_plugin_path = os.path.join(oc_plugin_dir, "context-pipe.ts")
|
|
202
|
+
|
|
203
|
+
oc_plugin_content = f"""/**
|
|
204
|
+
* Context-Pipe Native OpenCode Plugin
|
|
205
|
+
*/
|
|
206
|
+
export default function (api: any) {{
|
|
207
|
+
api.on("tool.execute.after", async (event: any) => {{
|
|
208
|
+
const rawContent = event.result;
|
|
209
|
+
if (typeof rawContent !== 'string' || rawContent.length < 500) return;
|
|
210
|
+
if (rawContent.includes("--- [Context-Pipe: Native Execution] ---")) return;
|
|
211
|
+
try {{
|
|
212
|
+
const pythonExe = "{os.path.abspath(sys.executable)}";
|
|
213
|
+
const payload = {{ hook_event_name: "AfterTool", tool_name: event.toolName, result: rawContent }};
|
|
214
|
+
const {{ execSync }} = require('child_process');
|
|
215
|
+
const response = execSync(`"${{pythonExe}}" -m context_pipe.orchestrator wrap`, {{ input: JSON.stringify(payload), encoding: 'utf-8' }});
|
|
216
|
+
const siftedData = JSON.parse(response);
|
|
217
|
+
if (siftedData?.result) {{
|
|
218
|
+
event.result = siftedData.result;
|
|
219
|
+
}}
|
|
220
|
+
}} catch (error) {{ console.error("[Context-Pipe Plugin] failed:", error); }}
|
|
221
|
+
}});
|
|
222
|
+
}};
|
|
223
|
+
"""
|
|
224
|
+
with open(oc_plugin_path, "w", encoding="utf-8") as f:
|
|
225
|
+
f.write(oc_plugin_content)
|
|
226
|
+
actions.append("Configured OpenCode native plugin.")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
actions.append(f"Failed to update opencode.json: {str(e)}")
|
|
230
|
+
|
|
231
|
+
# 5. Windsurf Security Gateway
|
|
232
|
+
if "windsurf" in env_lower:
|
|
233
|
+
windsurf_path = os.path.join(target_dir, ".windsurf", "hooks.json")
|
|
234
|
+
gateway_cmd = get_security_gateway_command()
|
|
235
|
+
if merge_hook_json(windsurf_path, "pre_mcp_tool_use", {
|
|
236
|
+
"matcher": "mcp__.*__(read_file|view_file)",
|
|
237
|
+
"type": "command",
|
|
238
|
+
"command": gateway_cmd
|
|
239
|
+
}):
|
|
240
|
+
actions.append("Injected Security Gateway into Windsurf hooks.")
|
|
241
|
+
|
|
242
|
+
# 6. Cline Security Gateway
|
|
243
|
+
if "cline" in env_lower:
|
|
244
|
+
cline_dir = os.path.join(target_dir, ".clinerules", "hooks")
|
|
245
|
+
os.makedirs(cline_dir, exist_ok=True)
|
|
246
|
+
ps1_blocker = """$inputJson = $input | ConvertFrom-Json
|
|
247
|
+
if ($inputJson.preToolUse.toolName -eq 'read_file' -or $inputJson.preToolUse.toolName -eq 'view_file') {
|
|
248
|
+
$filePath = $inputJson.preToolUse.parameters.path
|
|
249
|
+
if (Test-Path $filePath) {
|
|
250
|
+
$size = (Get-Item $filePath).Length
|
|
251
|
+
if ($size -gt 1024) {
|
|
252
|
+
$response = @{ cancel = $true; errorMessage = "[BLOCKED by Context-Pipe] File > 1KB. Use pipe_read_file instead." }
|
|
253
|
+
$response | ConvertTo-Json -Compress | Write-Output
|
|
254
|
+
exit 0
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
$response = @{ cancel = $false }
|
|
259
|
+
$response | ConvertTo-Json -Compress | Write-Output
|
|
260
|
+
"""
|
|
261
|
+
with open(os.path.join(cline_dir, "PreToolUse.ps1"), "w") as f:
|
|
262
|
+
f.write(ps1_blocker)
|
|
263
|
+
actions.append("Injected Security Gateway into Cline hooks (PS1).")
|
|
264
|
+
|
|
265
|
+
cline_bash_path = os.path.join(cline_dir, "PreToolUse")
|
|
266
|
+
cline_bash_content = """#!/bin/bash
|
|
267
|
+
INPUT=$(cat)
|
|
268
|
+
TOOL_NAME=$(echo "$INPUT" | grep -oP '(?<="toolName":")[^"]*')
|
|
269
|
+
if [[ "$TOOL_NAME" == "read_file" ]] || [[ "$TOOL_NAME" == "view_file" ]]; then
|
|
270
|
+
FILE_PATH=$(echo "$INPUT" | grep -oP '(?<="path":")[^"]*')
|
|
271
|
+
if [[ -f "$FILE_PATH" ]]; then
|
|
272
|
+
SIZE=$(wc -c < "$FILE_PATH" 2>/dev/null || stat -f %s "$FILE_PATH" 2>/dev/null || stat -c %s "$FILE_PATH" 2>/dev/null)
|
|
273
|
+
if [[ "$SIZE" -gt 1024 ]]; then
|
|
274
|
+
echo '{"cancel": true, "errorMessage": "[BLOCKED by Context-Pipe] File > 1KB. Use pipe_read_file instead."}'
|
|
275
|
+
exit 0
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
fi
|
|
279
|
+
echo '{"cancel": false}'
|
|
280
|
+
"""
|
|
281
|
+
with open(cline_bash_path, "w", encoding="utf-8", newline="\n") as f:
|
|
282
|
+
f.write(cline_bash_content)
|
|
283
|
+
try:
|
|
284
|
+
os.chmod(cline_bash_path, 0o755) # nosec B103
|
|
285
|
+
except OSError:
|
|
286
|
+
pass
|
|
287
|
+
actions.append("Injected Security Gateway into Cline hooks (Bash).")
|
|
288
|
+
|
|
289
|
+
# 7. Claude Code Injection
|
|
290
|
+
if "claude" in env_lower:
|
|
291
|
+
claude_paths = [
|
|
292
|
+
os.path.join(os.path.expanduser("~"), ".claude", "settings.json"),
|
|
293
|
+
os.path.join(target_dir, ".claude", "settings.json"),
|
|
294
|
+
]
|
|
295
|
+
for c_path in claude_paths:
|
|
296
|
+
if merge_hook_json(c_path, "PostToolUse", {"matcher": "mcp__.*__.*", "hooks": [{"type": "command", "command": cmd_str}]}):
|
|
297
|
+
actions.append(f"Merged into Claude Code hooks at {c_path}.")
|
|
298
|
+
|
|
299
|
+
# 8. Qwen CLI Injection
|
|
300
|
+
if "qwen" in env_lower:
|
|
301
|
+
qwen_paths = [
|
|
302
|
+
os.path.join(os.path.expanduser("~"), ".qwen", "settings.json"),
|
|
303
|
+
os.path.join(target_dir, ".qwen", "settings.json"),
|
|
304
|
+
]
|
|
305
|
+
for q_path in qwen_paths:
|
|
306
|
+
if merge_hook_json(q_path, "PostToolUse", {"matcher": "mcp__.*__.*", "hooks": [{"type": "command", "command": cmd_str}]}):
|
|
307
|
+
actions.append(f"Merged into Qwen CLI hooks at {q_path}.")
|
|
308
|
+
|
|
309
|
+
# 9. Codex CLI Injection
|
|
310
|
+
if "codex" in env_lower:
|
|
311
|
+
codex_paths = [
|
|
312
|
+
os.path.join(os.path.expanduser("~"), ".codex", "settings.json"),
|
|
313
|
+
os.path.join(target_dir, ".codex", "settings.json"),
|
|
314
|
+
]
|
|
315
|
+
for co_path in codex_paths:
|
|
316
|
+
if merge_hook_json(co_path, "PostToolUse", {"matcher": "mcp__.*__.*", "hooks": [{"type": "command", "command": cmd_str}]}):
|
|
317
|
+
actions.append(f"Merged into Codex CLI hooks at {co_path}.")
|
|
318
|
+
|
|
319
|
+
# 10. OpenClaw Injection
|
|
320
|
+
if "openclaw" in env_lower:
|
|
321
|
+
openclaw_plugin_path = os.path.join(target_dir, ".openclaw", "plugins", "context-pipe.ts")
|
|
322
|
+
os.makedirs(os.path.dirname(openclaw_plugin_path), exist_ok=True)
|
|
323
|
+
openclaw_plugin_content = f"""/**
|
|
324
|
+
* Context-Pipe Native OpenClaw Plugin
|
|
325
|
+
*/
|
|
326
|
+
export default function (api) {{
|
|
327
|
+
api.on("tool:after", async (event, ctx) => {{
|
|
328
|
+
const rawContent = ctx.result;
|
|
329
|
+
if (typeof rawContent !== 'string' || rawContent.length < 500) return;
|
|
330
|
+
if (rawContent.includes("--- [Context-Pipe: Native Execution] ---")) return;
|
|
331
|
+
try {{
|
|
332
|
+
const pythonExe = "{os.path.abspath(sys.executable)}";
|
|
333
|
+
const payload = {{ hook_event_name: "AfterTool", tool_name: ctx.toolName, tool_response: {{ llmContent: rawContent }} }};
|
|
334
|
+
const {{ execSync }} = require('child_process');
|
|
335
|
+
const response = execSync(`"${{pythonExe}}" -m context_pipe.orchestrator wrap`, {{ input: JSON.stringify(payload), encoding: 'utf-8' }});
|
|
336
|
+
const siftedData = JSON.parse(response);
|
|
337
|
+
if (siftedData?.tool_response?.llmContent) {{
|
|
338
|
+
ctx.result = siftedData.tool_response.llmContent;
|
|
339
|
+
}}
|
|
340
|
+
}} catch (error) {{ console.error("[Context-Pipe Plugin] failed:", error); }}
|
|
341
|
+
}});
|
|
342
|
+
}};
|
|
343
|
+
"""
|
|
344
|
+
with open(openclaw_plugin_path, "w", encoding="utf-8") as f:
|
|
345
|
+
f.write(openclaw_plugin_content)
|
|
346
|
+
actions.append("Configured OpenClaw native plugin.")
|
|
347
|
+
|
|
348
|
+
# 11. Kilo Code Injection
|
|
349
|
+
if "kilocode" in env_lower:
|
|
350
|
+
kilo_rule_dir = os.path.join(target_dir, ".kilocode", "rules")
|
|
351
|
+
os.makedirs(kilo_rule_dir, exist_ok=True)
|
|
352
|
+
kilo_rule_path = os.path.join(kilo_rule_dir, "context.md")
|
|
353
|
+
with open(kilo_rule_path, "w", encoding="utf-8") as f:
|
|
354
|
+
f.write("# Context-Pipe Kilo Code Constraints\n\nEnsure that all raw file reads use the `pipe_read_file` tool to prevent context flooding.")
|
|
355
|
+
actions.append("Injected Kilo Code workspace rules.")
|
|
356
|
+
|
|
357
|
+
return actions
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Luis Kobayashi. All rights reserved.
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
import os
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
# Metadata Signatures
|
|
12
|
+
CPP_SIGNATURE = "--- [Context-Pipe: Native Execution] ---"
|
|
13
|
+
|
|
14
|
+
def resolve_pipe_from_context(config: Dict[str, Any], tool_name: str, content_len: int) -> Optional[str]:
|
|
15
|
+
"""Resolves a pipe name based on mapping triggers."""
|
|
16
|
+
mappings = config.get("mappings", [])
|
|
17
|
+
|
|
18
|
+
for m in mappings:
|
|
19
|
+
trigger = m.get("trigger", "")
|
|
20
|
+
|
|
21
|
+
# 1. Tool Trigger (tool:regex)
|
|
22
|
+
if trigger.startswith("tool:"):
|
|
23
|
+
pattern = trigger.replace("tool:", "")
|
|
24
|
+
if re.search(pattern, tool_name, re.IGNORECASE):
|
|
25
|
+
return m["pipe"]
|
|
26
|
+
|
|
27
|
+
# 2. Size Trigger (size:>num)
|
|
28
|
+
if trigger.startswith("size:>"):
|
|
29
|
+
try:
|
|
30
|
+
threshold = int(trigger.replace("size:>", ""))
|
|
31
|
+
if content_len > threshold:
|
|
32
|
+
return m["pipe"]
|
|
33
|
+
except ValueError:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
# 3. Default Trigger
|
|
37
|
+
if trigger == "default":
|
|
38
|
+
return m["pipe"]
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def run_pipe(pipe_config: Dict[str, Any], input_data: str) -> tuple[str, List[Dict[str, Any]]]:
|
|
43
|
+
"""Executes a chain of nodes and tracks context deltas with a timeout guard."""
|
|
44
|
+
current_input = input_data
|
|
45
|
+
trace = []
|
|
46
|
+
|
|
47
|
+
# Global timeout for the entire pipe execution (default 10s)
|
|
48
|
+
raw_timeout = os.environ.get("PIPE_NODE_TIMEOUT_MS", "10000")
|
|
49
|
+
node_timeout = int(raw_timeout) / 1000.0
|
|
50
|
+
|
|
51
|
+
for node in pipe_config.get("nodes", []):
|
|
52
|
+
use_shell = node.get("shell", False)
|
|
53
|
+
|
|
54
|
+
cmd: str | List[str]
|
|
55
|
+
if use_shell:
|
|
56
|
+
# Join cmd and args for shell execution
|
|
57
|
+
cmd = " ".join([node["cmd"]] + [str(a) for a in node.get("args", [])])
|
|
58
|
+
else:
|
|
59
|
+
cmd = [node["cmd"]] + [str(a) for a in node.get("args", [])]
|
|
60
|
+
|
|
61
|
+
start_size = len(current_input)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# High-Fidelity OS Piping
|
|
65
|
+
process = subprocess.Popen(
|
|
66
|
+
cmd,
|
|
67
|
+
stdin=subprocess.PIPE,
|
|
68
|
+
stdout=subprocess.PIPE,
|
|
69
|
+
stderr=subprocess.PIPE,
|
|
70
|
+
text=True,
|
|
71
|
+
shell=use_shell # nosec B602
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
stdout, stderr = process.communicate(input=current_input, timeout=node_timeout)
|
|
76
|
+
except subprocess.TimeoutExpired:
|
|
77
|
+
process.kill()
|
|
78
|
+
stdout, stderr = process.communicate()
|
|
79
|
+
error_text = f"--- [Context-Pipe: Timeout] ---\nNode {node['cmd']} exceeded {node_timeout}s."
|
|
80
|
+
trace.append({"node": node["cmd"], "error": "Timeout"})
|
|
81
|
+
return error_text, trace
|
|
82
|
+
|
|
83
|
+
if process.returncode != 0:
|
|
84
|
+
# Record error in trace
|
|
85
|
+
trace.append({
|
|
86
|
+
"node": node["cmd"],
|
|
87
|
+
"error": stderr.strip()
|
|
88
|
+
})
|
|
89
|
+
return f"Error in node {node['cmd']}: {stderr}", trace
|
|
90
|
+
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
help_msg = node.get("help_msg", f"Command '{node['cmd']}' not found in system PATH.")
|
|
93
|
+
error_text = f"--- [Context-Pipe: Dependency Error] ---\n{help_msg}"
|
|
94
|
+
trace.append({
|
|
95
|
+
"node": node["cmd"],
|
|
96
|
+
"error": "FileNotFound"
|
|
97
|
+
})
|
|
98
|
+
return error_text, trace
|
|
99
|
+
|
|
100
|
+
end_size = len(stdout)
|
|
101
|
+
trace.append({
|
|
102
|
+
"node": node["cmd"],
|
|
103
|
+
"input_size": start_size,
|
|
104
|
+
"output_size": end_size,
|
|
105
|
+
"delta": end_size - start_size
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
current_input = stdout
|
|
109
|
+
|
|
110
|
+
return current_input, trace
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
parser = argparse.ArgumentParser(description="Context-Pipe Orchestrator")
|
|
114
|
+
subparsers = parser.add_subparsers(dest="command", help="Subcommands")
|
|
115
|
+
|
|
116
|
+
# 1. 'run' command (default)
|
|
117
|
+
run_parser = subparsers.add_parser("run", help="Run a specific pipe")
|
|
118
|
+
run_parser.add_argument("pipe_name", help="Name of the pipe to execute from pipes.json")
|
|
119
|
+
run_parser.add_argument("--config", default="pipes.json", help="Path to pipes.json")
|
|
120
|
+
|
|
121
|
+
# 2. 'wrap' command (JSON polyfill)
|
|
122
|
+
wrap_parser = subparsers.add_parser("wrap", help="Wrap a JSON-RPC payload")
|
|
123
|
+
wrap_parser.add_argument("--config", default="pipes.json", help="Path to pipes.json")
|
|
124
|
+
|
|
125
|
+
# Compatibility with old behavior (no subcommand)
|
|
126
|
+
if len(sys.argv) > 1 and sys.argv[1] not in ["run", "wrap"]:
|
|
127
|
+
sys.argv.insert(1, "run")
|
|
128
|
+
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
with open(args.config, "r") as f:
|
|
133
|
+
config = json.load(f)
|
|
134
|
+
except FileNotFoundError:
|
|
135
|
+
print(f"Error: Config file {args.config} not found.")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
if args.command == "run":
|
|
139
|
+
# Find the requested pipe
|
|
140
|
+
pipe = next((p for p in config.get("pipes", []) if p["name"] == args.pipe_name), None)
|
|
141
|
+
|
|
142
|
+
if not pipe:
|
|
143
|
+
print(f"Error: Pipe '{args.pipe_name}' not found in {args.config}")
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
# Read from stdin
|
|
147
|
+
input_data = sys.stdin.read()
|
|
148
|
+
|
|
149
|
+
# Run the pipe
|
|
150
|
+
result, trace = run_pipe(pipe, input_data)
|
|
151
|
+
|
|
152
|
+
# Output the result
|
|
153
|
+
sys.stdout.write(result)
|
|
154
|
+
|
|
155
|
+
elif args.command == "wrap":
|
|
156
|
+
from .wrapper import wrap_payload
|
|
157
|
+
raw_input = sys.stdin.read()
|
|
158
|
+
result = wrap_payload(raw_input, config)
|
|
159
|
+
sys.stdout.write(result)
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Luis Kobayashi. All rights reserved.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import psutil
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
def detect_client_id() -> str:
|
|
9
|
+
"""
|
|
10
|
+
Infers the calling IDE/CLI from environment variables and parent process name.
|
|
11
|
+
Prioritized for accurate attribution.
|
|
12
|
+
"""
|
|
13
|
+
# 1. Environment Variable Fingerprints
|
|
14
|
+
_ENV_MAP = [
|
|
15
|
+
("ANTIGRAVITY_AGENT", "Google Antigravity"),
|
|
16
|
+
("OPENCODE", "OpenCode"),
|
|
17
|
+
("OPENCODE_PID", "OpenCode"),
|
|
18
|
+
("CURSOR_TRACE_ID", "Cursor"),
|
|
19
|
+
("VSCODE_PID", "VSCode"),
|
|
20
|
+
("WINDSURF_TOOL_ARGS", "Windsurf"),
|
|
21
|
+
("__KIRO_MCP", "Kiro"),
|
|
22
|
+
("CONTINUE_SERVER_PORT", "Continue"),
|
|
23
|
+
("JETBRAINS_IDE_URL", "JetBrains"),
|
|
24
|
+
("CLINE_TASK_ID", "Cline"),
|
|
25
|
+
("CLAUDE_TOOL_NAME", "Claude Desktop"),
|
|
26
|
+
("GEMINI_SESSION_ID", "Gemini CLI")
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
for var, label in _ENV_MAP:
|
|
30
|
+
if os.environ.get(var):
|
|
31
|
+
return label
|
|
32
|
+
|
|
33
|
+
# 2. Parent Process Heuristics
|
|
34
|
+
_PROC_MAP = [
|
|
35
|
+
("antigravity", "Google Antigravity"),
|
|
36
|
+
("opencode", "OpenCode"),
|
|
37
|
+
("cursor", "Cursor"),
|
|
38
|
+
("windsurf", "Windsurf"),
|
|
39
|
+
("claude", "Claude Desktop"),
|
|
40
|
+
("gemini", "Gemini CLI"),
|
|
41
|
+
("cline", "Cline"),
|
|
42
|
+
("jetbrains", "JetBrains"),
|
|
43
|
+
("zed", "Zed")
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
proc = psutil.Process(os.getpid())
|
|
48
|
+
for ancestor in [proc] + proc.parents():
|
|
49
|
+
try:
|
|
50
|
+
name = ancestor.name().lower()
|
|
51
|
+
for fragment, label in _PROC_MAP:
|
|
52
|
+
if fragment in name:
|
|
53
|
+
return label
|
|
54
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
55
|
+
continue
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return "Generic CLI"
|
|
60
|
+
|
|
61
|
+
def extract_content(data: Dict, platform: str) -> tuple[str, Optional[str], Optional[str]]:
|
|
62
|
+
"""
|
|
63
|
+
Extracts raw text, tool name, and agent label from a payload.
|
|
64
|
+
Agent labels identify sub-threads or specialized agent types.
|
|
65
|
+
"""
|
|
66
|
+
tool_name = data.get("tool_name") or data.get("tool") or "unknown"
|
|
67
|
+
agent_label = None
|
|
68
|
+
content = ""
|
|
69
|
+
|
|
70
|
+
# Platform-specific subagent detection
|
|
71
|
+
if platform == "Cursor":
|
|
72
|
+
res = data.get("result", "")
|
|
73
|
+
if isinstance(res, str):
|
|
74
|
+
if res.startswith("[Explore]"):
|
|
75
|
+
agent_label = "Explore"
|
|
76
|
+
elif res.startswith("[Bash]"):
|
|
77
|
+
agent_label = "Bash"
|
|
78
|
+
elif platform == "Gemini CLI":
|
|
79
|
+
agent_label = data.get("hookSpecificOutput", {}).get("threadLabel")
|
|
80
|
+
|
|
81
|
+
# Shape-Aware Extraction
|
|
82
|
+
if "tool_response" in data and isinstance(data["tool_response"], dict):
|
|
83
|
+
content = data["tool_response"].get("llmContent", "")
|
|
84
|
+
if not content:
|
|
85
|
+
content = data.get("result", "")
|
|
86
|
+
if not content:
|
|
87
|
+
content = data.get("llmContent") or data.get("content") or ""
|
|
88
|
+
|
|
89
|
+
return content, tool_name, agent_label
|
|
90
|
+
|
|
91
|
+
def inject_content(data: Dict, content: str, platform: str) -> Dict:
|
|
92
|
+
"""
|
|
93
|
+
Injects processed content back into the platform-specific JSON payload.
|
|
94
|
+
"""
|
|
95
|
+
# 1. Standard MCP / VSCode / Gemini / OpenCode Shape
|
|
96
|
+
if "tool_response" in data and isinstance(data["tool_response"], dict):
|
|
97
|
+
data["tool_response"]["llmContent"] = content
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
# 2. Cursor / Claude Desktop / CLI Shape
|
|
101
|
+
if "result" in data:
|
|
102
|
+
data["result"] = content
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
# 3. Fallback: if 'llmContent' exists directly
|
|
106
|
+
if "llmContent" in data:
|
|
107
|
+
data["llmContent"] = content
|
|
108
|
+
else:
|
|
109
|
+
# 4. Universal key for unrecognized shapes
|
|
110
|
+
data["processed_content"] = content
|
|
111
|
+
|
|
112
|
+
return data
|