monoco-toolkit 0.3.11__py3-none-any.whl → 0.4.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.
Files changed (132) hide show
  1. monoco/core/automation/__init__.py +40 -0
  2. monoco/core/automation/field_watcher.py +296 -0
  3. monoco/core/automation/handlers.py +805 -0
  4. monoco/core/config.py +29 -11
  5. monoco/core/daemon/__init__.py +5 -0
  6. monoco/core/daemon/pid.py +290 -0
  7. monoco/core/git.py +15 -0
  8. monoco/core/hooks/context.py +74 -13
  9. monoco/core/injection.py +86 -8
  10. monoco/core/integrations.py +0 -24
  11. monoco/core/router/__init__.py +17 -0
  12. monoco/core/router/action.py +202 -0
  13. monoco/core/scheduler/__init__.py +63 -0
  14. monoco/core/scheduler/base.py +152 -0
  15. monoco/core/scheduler/engines.py +175 -0
  16. monoco/core/scheduler/events.py +197 -0
  17. monoco/core/scheduler/local.py +377 -0
  18. monoco/core/setup.py +9 -0
  19. monoco/core/sync.py +199 -4
  20. monoco/core/watcher/__init__.py +63 -0
  21. monoco/core/watcher/base.py +382 -0
  22. monoco/core/watcher/dropzone.py +152 -0
  23. monoco/core/watcher/im.py +460 -0
  24. monoco/core/watcher/issue.py +303 -0
  25. monoco/core/watcher/memo.py +192 -0
  26. monoco/core/watcher/task.py +238 -0
  27. monoco/daemon/app.py +3 -60
  28. monoco/daemon/commands.py +459 -25
  29. monoco/daemon/events.py +34 -0
  30. monoco/daemon/scheduler.py +157 -201
  31. monoco/daemon/services.py +42 -243
  32. monoco/features/agent/__init__.py +25 -7
  33. monoco/features/agent/cli.py +91 -57
  34. monoco/features/agent/engines.py +31 -170
  35. monoco/features/agent/resources/en/AGENTS.md +14 -14
  36. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  37. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  38. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  39. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  40. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  41. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  42. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  43. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  44. monoco/features/agent/worker.py +1 -1
  45. monoco/features/hooks/__init__.py +61 -6
  46. monoco/features/hooks/commands.py +281 -271
  47. monoco/features/hooks/dispatchers/__init__.py +23 -0
  48. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  49. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  50. monoco/features/hooks/manager.py +357 -0
  51. monoco/features/hooks/models.py +262 -0
  52. monoco/features/hooks/parser.py +322 -0
  53. monoco/features/hooks/universal_interceptor.py +503 -0
  54. monoco/features/im/__init__.py +67 -0
  55. monoco/features/im/core.py +782 -0
  56. monoco/features/im/models.py +311 -0
  57. monoco/features/issue/commands.py +133 -60
  58. monoco/features/issue/core.py +385 -40
  59. monoco/features/issue/domain_commands.py +0 -19
  60. monoco/features/issue/resources/en/AGENTS.md +17 -122
  61. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  62. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  63. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  64. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  65. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  66. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  67. monoco/features/memo/cli.py +15 -64
  68. monoco/features/memo/core.py +6 -34
  69. monoco/features/memo/models.py +24 -15
  70. monoco/features/memo/resources/en/AGENTS.md +31 -0
  71. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  72. monoco/features/spike/commands.py +5 -3
  73. monoco/main.py +5 -3
  74. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  75. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  76. monoco/core/execution.py +0 -67
  77. monoco/features/agent/apoptosis.py +0 -44
  78. monoco/features/agent/manager.py +0 -127
  79. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  80. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  81. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  82. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  83. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  84. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  85. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  86. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  87. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  88. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  89. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  90. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  91. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  92. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  93. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  94. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  95. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  96. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  97. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  98. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  99. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  100. monoco/features/agent/session.py +0 -169
  101. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  102. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  103. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  104. monoco/features/hooks/adapter.py +0 -67
  105. monoco/features/hooks/core.py +0 -441
  106. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  107. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  108. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  109. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  110. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  111. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  112. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  113. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  114. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  115. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  116. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  117. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  118. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  119. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  120. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  121. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  122. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  123. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  124. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  125. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  126. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  127. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  128. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  129. monoco_toolkit-0.3.11.dist-info/RECORD +0 -181
  130. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  131. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  132. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,503 @@
1
+ """
2
+ Universal Interceptor for Agent Hooks (ACL - Anti-Corruption Layer).
3
+
4
+ This module provides runtime protocol translation between different Agent platforms
5
+ (Claude Code, Gemini CLI) and the Monoco unified hook protocol.
6
+
7
+ Usage:
8
+ python -m monoco.features.hooks.universal_interceptor <hook_script_path>
9
+
10
+ The interceptor:
11
+ 1. Auto-detects the Agent platform from input format
12
+ 2. Translates agent-specific input to Monoco unified format
13
+ 3. Executes the actual hook script
14
+ 4. Translates the output back to agent-specific format
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ from abc import ABC, abstractmethod
22
+ from dataclasses import dataclass, field
23
+ from enum import Enum
24
+ from pathlib import Path
25
+ from typing import Any, Optional
26
+
27
+
28
+ class AgentProvider(Enum):
29
+ """Supported Agent providers."""
30
+ CLAUDE_CODE = "claude-code"
31
+ GEMINI_CLI = "gemini-cli"
32
+ UNKNOWN = "unknown"
33
+
34
+
35
+ @dataclass
36
+ class UnifiedDecision:
37
+ """
38
+ Unified decision model for hook responses.
39
+
40
+ This is the internal format used by Monoco, independent of any specific agent.
41
+ """
42
+ decision: str # "allow", "deny", "ask"
43
+ reason: str = ""
44
+ message: str = ""
45
+ metadata: dict[str, Any] = field(default_factory=dict)
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ """Convert to dictionary."""
49
+ return {
50
+ "decision": self.decision,
51
+ "reason": self.reason,
52
+ "message": self.message,
53
+ "metadata": self.metadata,
54
+ }
55
+
56
+ @classmethod
57
+ def from_dict(cls, data: dict[str, Any]) -> "UnifiedDecision":
58
+ """Create from dictionary."""
59
+ return cls(
60
+ decision=data.get("decision", "ask"),
61
+ reason=data.get("reason", ""),
62
+ message=data.get("message", ""),
63
+ metadata=data.get("metadata", {}),
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class UnifiedHookInput:
69
+ """
70
+ Unified hook input model.
71
+
72
+ This normalizes input from different agent platforms into a common format.
73
+ """
74
+ event: str # Monoco event name (e.g., "before-tool", "before-agent")
75
+ tool: Optional[str] = None # Tool name (for tool-related events)
76
+ input_data: dict[str, Any] = field(default_factory=dict) # Tool/agent input
77
+ env: dict[str, str] = field(default_factory=dict) # Environment variables
78
+ metadata: dict[str, Any] = field(default_factory=dict) # Additional metadata
79
+
80
+ def to_dict(self) -> dict[str, Any]:
81
+ """Convert to dictionary."""
82
+ return {
83
+ "event": self.event,
84
+ "tool": self.tool,
85
+ "input": self.input_data,
86
+ "env": self.env,
87
+ "metadata": self.metadata,
88
+ }
89
+
90
+
91
+ class AgentAdapter(ABC):
92
+ """
93
+ Abstract base class for Agent-specific adapters.
94
+
95
+ Adapters handle the translation between agent-specific protocols
96
+ and the Monoco unified protocol.
97
+ """
98
+
99
+ def __init__(self, provider: AgentProvider):
100
+ self.provider = provider
101
+
102
+ @abstractmethod
103
+ def detect(self, raw_input: str) -> bool:
104
+ """
105
+ Detect if this adapter should be used based on input format.
106
+
107
+ Args:
108
+ raw_input: Raw JSON string from stdin
109
+
110
+ Returns:
111
+ True if this adapter should handle the input format
112
+ """
113
+ pass
114
+
115
+ @abstractmethod
116
+ def translate_input(self, raw_input: str) -> UnifiedHookInput:
117
+ """
118
+ Translate agent-specific input to unified format.
119
+
120
+ Args:
121
+ raw_input: Raw JSON string from stdin
122
+
123
+ Returns:
124
+ UnifiedHookInput
125
+ """
126
+ pass
127
+
128
+ @abstractmethod
129
+ def translate_output(self, decision: UnifiedDecision) -> str:
130
+ """
131
+ Translate unified decision to agent-specific output.
132
+
133
+ Args:
134
+ decision: Unified decision
135
+
136
+ Returns:
137
+ JSON string for agent-specific output
138
+ """
139
+ pass
140
+
141
+
142
+ class ClaudeAdapter(AgentAdapter):
143
+ """
144
+ Adapter for Claude Code agent.
145
+
146
+ Event Mapping (Claude -> Monoco):
147
+ - PreToolUse -> before-tool
148
+ - PostToolUse -> after-tool
149
+ - UserPromptSubmit -> before-agent
150
+ - Stop -> after-agent
151
+ - SessionStart -> session-start
152
+ - SessionEnd -> session-end
153
+
154
+ Decision Mapping:
155
+ - Claude "permissionDecision" -> Monoco "decision"
156
+ - Values: "allow", "deny", "ask"
157
+ """
158
+
159
+ # Event name mapping: Claude -> Monoco
160
+ EVENT_MAP = {
161
+ "PreToolUse": "before-tool",
162
+ "PostToolUse": "after-tool",
163
+ "UserPromptSubmit": "before-agent",
164
+ "Stop": "after-agent",
165
+ "SessionStart": "session-start",
166
+ "SessionEnd": "session-end",
167
+ }
168
+
169
+ # Reverse mapping: Monoco -> Claude
170
+ REVERSE_EVENT_MAP = {v: k for k, v in EVENT_MAP.items()}
171
+
172
+ def __init__(self):
173
+ super().__init__(AgentProvider.CLAUDE_CODE)
174
+
175
+ def detect(self, raw_input: str) -> bool:
176
+ """Detect if input is in Claude Code format."""
177
+ try:
178
+ data = json.loads(raw_input)
179
+ # Try both old and new field names
180
+ event = data.get("hook_event_name") or data.get("event", "")
181
+ # Claude Code uses specific event names
182
+ return event in self.EVENT_MAP
183
+ except json.JSONDecodeError:
184
+ return False
185
+
186
+ def translate_input(self, raw_input: str) -> UnifiedHookInput:
187
+ """
188
+ Translate Claude Code input to unified format.
189
+
190
+ Claude Code input format:
191
+ {
192
+ "hook_event_name": "PreToolUse",
193
+ "tool_name": "Bash",
194
+ "tool_input": {"command": "ls -la"},
195
+ ...
196
+ }
197
+ """
198
+ try:
199
+ data = json.loads(raw_input)
200
+ except json.JSONDecodeError:
201
+ data = {}
202
+
203
+ # Use real Claude Code field names (hook_event_name, tool_name, tool_input)
204
+ claude_event = data.get("hook_event_name") or data.get("event", "")
205
+ monoco_event = self.EVENT_MAP.get(claude_event, claude_event.lower())
206
+
207
+ return UnifiedHookInput(
208
+ event=monoco_event,
209
+ tool=data.get("tool_name") or data.get("tool"),
210
+ input_data=data.get("tool_input") or data.get("input", {}),
211
+ env=dict(os.environ),
212
+ metadata={k: v for k, v in data.items() if k not in ("hook_event_name", "event", "tool_name", "tool", "tool_input", "input")},
213
+ )
214
+
215
+ def translate_output(self, decision: UnifiedDecision) -> str:
216
+ """
217
+ Translate unified decision to Claude Code output format.
218
+
219
+ Claude Code output format:
220
+ {
221
+ "permissionDecision": "allow" | "deny" | "ask",
222
+ "reason": "...",
223
+ "message": "..."
224
+ }
225
+ """
226
+ output = {
227
+ "permissionDecision": decision.decision,
228
+ "reason": decision.reason,
229
+ "message": decision.message,
230
+ }
231
+ return json.dumps(output)
232
+
233
+
234
+ class GeminiAdapter(AgentAdapter):
235
+ """
236
+ Adapter for Gemini CLI agent.
237
+
238
+ Event Mapping (Gemini -> Monoco):
239
+ - BeforeTool -> before-tool
240
+ - AfterTool -> after-tool
241
+ - BeforeAgent -> before-agent
242
+ - AfterAgent -> after-agent
243
+ - SessionStart -> session-start
244
+ - SessionEnd -> session-end
245
+
246
+ Decision Mapping:
247
+ - Gemini "decision" -> Monoco "decision"
248
+ - Values: "allow", "deny", "ask"
249
+ """
250
+
251
+ # Event name mapping: Gemini -> Monoco
252
+ EVENT_MAP = {
253
+ "BeforeTool": "before-tool",
254
+ "AfterTool": "after-tool",
255
+ "BeforeAgent": "before-agent",
256
+ "AfterAgent": "after-agent",
257
+ "SessionStart": "session-start",
258
+ "SessionEnd": "session-end",
259
+ "BeforeModel": "before-model",
260
+ "AfterModel": "after-model",
261
+ "BeforeToolSelection": "before-tool-selection",
262
+ "Notification": "notification",
263
+ }
264
+
265
+ # Reverse mapping: Monoco -> Gemini
266
+ REVERSE_EVENT_MAP = {v: k for k, v in EVENT_MAP.items()}
267
+
268
+ def __init__(self):
269
+ super().__init__(AgentProvider.GEMINI_CLI)
270
+
271
+ def detect(self, raw_input: str) -> bool:
272
+ """Detect if input is in Gemini CLI format."""
273
+ try:
274
+ data = json.loads(raw_input)
275
+ # Gemini CLI uses hook_event_name or event
276
+ event = data.get("hook_event_name") or data.get("event", "")
277
+ # Gemini CLI uses specific event names
278
+ return event in self.EVENT_MAP
279
+ except json.JSONDecodeError:
280
+ return False
281
+
282
+ def translate_input(self, raw_input: str) -> UnifiedHookInput:
283
+ """
284
+ Translate Gemini CLI input to unified format.
285
+
286
+ Gemini CLI input format:
287
+ {
288
+ "hook_event_name": "BeforeTool",
289
+ "tool_name": "Bash",
290
+ "tool_input": {"command": "ls -la"},
291
+ ...
292
+ }
293
+ """
294
+ try:
295
+ data = json.loads(raw_input)
296
+ except json.JSONDecodeError:
297
+ data = {}
298
+
299
+ gemini_event = data.get("hook_event_name") or data.get("event", "")
300
+ monoco_event = self.EVENT_MAP.get(gemini_event, gemini_event.lower())
301
+
302
+ return UnifiedHookInput(
303
+ event=monoco_event,
304
+ tool=data.get("tool_name") or data.get("tool"),
305
+ input_data=data.get("tool_input") or data.get("input", {}),
306
+ env=dict(os.environ),
307
+ metadata={k: v for k, v in data.items() if k not in ("hook_event_name", "event", "tool_name", "tool", "tool_input", "input")},
308
+ )
309
+
310
+ def translate_output(self, decision: UnifiedDecision) -> str:
311
+ """
312
+ Translate unified decision to Gemini CLI output format.
313
+
314
+ Gemini CLI output format:
315
+ {
316
+ "decision": "allow" | "deny" | "ask",
317
+ "reason": "...",
318
+ "message": "..."
319
+ }
320
+ """
321
+ output = {
322
+ "decision": decision.decision,
323
+ "reason": decision.reason,
324
+ "message": decision.message,
325
+ }
326
+ return json.dumps(output)
327
+
328
+
329
+ class UniversalInterceptor:
330
+ """
331
+ Universal interceptor for agent hooks.
332
+
333
+ This is the main entry point for the ACL runtime. It:
334
+ 1. Auto-detects the agent platform
335
+ 2. Translates input from agent-specific to unified format
336
+ 3. Executes the hook script with unified input
337
+ 4. Translates output back to agent-specific format
338
+ """
339
+
340
+ def __init__(self):
341
+ self.adapters: list[AgentAdapter] = [
342
+ ClaudeAdapter(),
343
+ GeminiAdapter(),
344
+ ]
345
+ self.adapter: Optional[AgentAdapter] = None
346
+
347
+ def detect_adapter(self, raw_input: str) -> AgentAdapter:
348
+ """
349
+ Auto-detect the appropriate adapter based on input format.
350
+
351
+ Args:
352
+ raw_input: Raw JSON string from stdin
353
+
354
+ Returns:
355
+ The detected adapter
356
+
357
+ Raises:
358
+ RuntimeError: If no adapter can be detected
359
+ """
360
+ for adapter in self.adapters:
361
+ if adapter.detect(raw_input):
362
+ return adapter
363
+
364
+ raise RuntimeError(
365
+ "Could not detect agent input format. "
366
+ "Expected Claude Code or Gemini CLI event format."
367
+ )
368
+
369
+ def run(self, hook_script_path: str, is_debug: bool = False, stdin_data: Optional[str] = None) -> int:
370
+ """
371
+ Run the interceptor.
372
+
373
+ Args:
374
+ hook_script_path: Path to the actual hook script to execute
375
+ is_debug: Whether debug mode is enabled for this hook
376
+ stdin_data: Optional stdin data (if already read by caller)
377
+
378
+ Returns:
379
+ Exit code (0 for success, non-zero for failure)
380
+ """
381
+ # 1. Read raw input from stdin (or use provided data)
382
+ raw_input = stdin_data if stdin_data is not None else sys.stdin.read()
383
+
384
+ # 2. Detect adapter based on input format
385
+ try:
386
+ adapter = self.detect_adapter(raw_input)
387
+ except RuntimeError as e:
388
+ print(json.dumps({"error": str(e)}), file=sys.stderr)
389
+ return 1
390
+
391
+ # 3. Translate input to unified format
392
+ unified_input = adapter.translate_input(raw_input)
393
+
394
+ # 4. Execute the hook script
395
+ try:
396
+ unified_output = self._execute_hook(
397
+ hook_script_path, unified_input, adapter.provider.value, is_debug
398
+ )
399
+ except subprocess.CalledProcessError as e:
400
+ # Script failed - return deny decision
401
+ unified_output = UnifiedDecision(
402
+ decision="deny",
403
+ reason=f"Hook script failed: {e}",
404
+ message="The hook script encountered an error.",
405
+ )
406
+ except Exception as e:
407
+ print(json.dumps({"error": str(e)}), file=sys.stderr)
408
+ return 1
409
+
410
+ # 5. Translate output to agent-specific format
411
+ agent_output = adapter.translate_output(unified_output)
412
+
413
+ # 6. Print output for the agent
414
+ print(agent_output)
415
+ return 0
416
+
417
+ def _execute_hook(
418
+ self, script_path: str, unified_input: UnifiedHookInput, provider: str, is_debug: bool = False
419
+ ) -> UnifiedDecision:
420
+ """
421
+ Execute the hook script with unified input.
422
+
423
+ Args:
424
+ script_path: Path to the hook script
425
+ unified_input: Unified input data
426
+ provider: The detected provider name
427
+
428
+ Returns:
429
+ Unified decision from the script
430
+ """
431
+ script_path = Path(script_path)
432
+ if not script_path.exists():
433
+ # If script doesn't exist, allow by default
434
+ return UnifiedDecision(decision="allow")
435
+
436
+ # Prepare environment
437
+ env = os.environ.copy()
438
+ env["MONOCO_HOOK_EVENT"] = unified_input.event
439
+ env["MONOCO_HOOK_PROVIDER"] = provider
440
+ env["MONOCO_HOOK_TYPE"] = "agent"
441
+
442
+ # Add tool info if available
443
+ if unified_input.tool:
444
+ env["MONOCO_HOOK_TOOL"] = unified_input.tool
445
+
446
+ # Propagate debug flag if set
447
+ if os.environ.get("MONOCO_HOOK_DEBUG") or is_debug:
448
+ env["MONOCO_HOOK_DEBUG"] = "1"
449
+ env["MONOCO_HOOK_IS_DEBUG"] = "true"
450
+
451
+ # Execute the script
452
+ input_json = json.dumps(unified_input.to_dict())
453
+
454
+ result = subprocess.run(
455
+ [str(script_path)],
456
+ input=input_json,
457
+ capture_output=True,
458
+ text=True,
459
+ env=env,
460
+ timeout=300, # 5 minute timeout
461
+ )
462
+
463
+ # Parse output
464
+ if result.returncode == 0 and result.stdout.strip():
465
+ try:
466
+ output_data = json.loads(result.stdout.strip())
467
+ return UnifiedDecision.from_dict(output_data)
468
+ except json.JSONDecodeError:
469
+ # Non-JSON output - treat as allow with message
470
+ return UnifiedDecision(
471
+ decision="allow",
472
+ message=result.stdout.strip(),
473
+ )
474
+ elif result.returncode != 0:
475
+ # Script failed - deny
476
+ return UnifiedDecision(
477
+ decision="deny",
478
+ reason=f"Hook script exited with code {result.returncode}",
479
+ message=result.stderr.strip() or "Hook script failed",
480
+ )
481
+ else:
482
+ # No output - allow by default
483
+ return UnifiedDecision(decision="allow")
484
+
485
+
486
+ def main() -> int:
487
+ """
488
+ Main entry point for the universal interceptor.
489
+
490
+ Usage:
491
+ python -m monoco.features.hooks.universal_interceptor <hook_script_path>
492
+ """
493
+ if len(sys.argv) < 2:
494
+ print("Usage: universal_interceptor <hook_script_path>", file=sys.stderr)
495
+ return 1
496
+
497
+ hook_script_path = sys.argv[1]
498
+ interceptor = UniversalInterceptor()
499
+ return interceptor.run(hook_script_path)
500
+
501
+
502
+ if __name__ == "__main__":
503
+ sys.exit(main())
@@ -0,0 +1,67 @@
1
+ """
2
+ IM (Instant Messaging) Infrastructure (FEAT-0167).
3
+
4
+ Provides core data models, storage, and routing for IM system integration.
5
+
6
+ Example:
7
+ >>> from monoco.features.im import IMManager
8
+ >>> im = IMManager(project_root)
9
+ >>> channel = im.channels.create_channel("group123", PlatformType.FEISHU, name="Test Group")
10
+ >>> message = IMMessage(message_id="msg1", channel_id="group123", ...)
11
+ >>> im.messages.save_message(message)
12
+ """
13
+
14
+ from .models import (
15
+ PlatformType,
16
+ ChannelType,
17
+ MessageStatus,
18
+ ParticipantType,
19
+ ContentType,
20
+ IMParticipant,
21
+ Attachment,
22
+ MessageContent,
23
+ ProcessingStep,
24
+ IMMessage,
25
+ IMChannel,
26
+ IMAgentSession,
27
+ IMWebhookConfig,
28
+ IMStats,
29
+ )
30
+
31
+ from .core import (
32
+ IMManager,
33
+ IMChannelManager,
34
+ MessageStore,
35
+ IMRouter,
36
+ IMAgentSessionManager,
37
+ IMStorageError,
38
+ ChannelNotFoundError,
39
+ MessageNotFoundError,
40
+ )
41
+
42
+ __all__ = [
43
+ # Models
44
+ "PlatformType",
45
+ "ChannelType",
46
+ "MessageStatus",
47
+ "ParticipantType",
48
+ "ContentType",
49
+ "IMParticipant",
50
+ "Attachment",
51
+ "MessageContent",
52
+ "ProcessingStep",
53
+ "IMMessage",
54
+ "IMChannel",
55
+ "IMAgentSession",
56
+ "IMWebhookConfig",
57
+ "IMStats",
58
+ # Core
59
+ "IMManager",
60
+ "IMChannelManager",
61
+ "MessageStore",
62
+ "IMRouter",
63
+ "IMAgentSessionManager",
64
+ "IMStorageError",
65
+ "ChannelNotFoundError",
66
+ "MessageNotFoundError",
67
+ ]