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.
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026 Luis Kobayashi. All rights reserved.
@@ -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