conduct-cli 0.6.18__tar.gz → 0.6.20__tar.gz
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.
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/PKG-INFO +1 -1
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/pyproject.toml +1 -1
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/base.py +18 -2
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/session_start.py +79 -1
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/README.md +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/setup.cfg +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/setup.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_log_util.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.6.18 → conduct_cli-0.6.20}/tests/test_switch.py +0 -0
|
@@ -159,10 +159,18 @@ def run_drain_daemon() -> None:
|
|
|
159
159
|
if isinstance(entry["payload"], str)
|
|
160
160
|
else entry["payload"]
|
|
161
161
|
)
|
|
162
|
+
try:
|
|
163
|
+
from importlib.metadata import version as _pkg_ver
|
|
164
|
+
_ua = f"conduct-cli/{_pkg_ver('conduct-cli')}"
|
|
165
|
+
except Exception:
|
|
166
|
+
_ua = "conduct-cli"
|
|
162
167
|
req = urllib.request.Request(
|
|
163
168
|
f"{api_url}/guard/events",
|
|
164
169
|
data=payload,
|
|
165
|
-
headers={
|
|
170
|
+
headers={
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
"User-Agent": _ua,
|
|
173
|
+
},
|
|
166
174
|
method="POST",
|
|
167
175
|
)
|
|
168
176
|
urllib.request.urlopen(req, timeout=8)
|
|
@@ -181,6 +189,14 @@ def run_drain_daemon() -> None:
|
|
|
181
189
|
pass
|
|
182
190
|
|
|
183
191
|
|
|
192
|
+
_WAF_CHARS = str.maketrans("", "", "|;&$`<>")
|
|
193
|
+
|
|
194
|
+
def _safe_summary(tool_input: dict) -> str:
|
|
195
|
+
"""Summarise tool input, stripping shell metacharacters that trigger WAF rules."""
|
|
196
|
+
raw = json.dumps(tool_input)[:300]
|
|
197
|
+
return raw.translate(_WAF_CHARS)[:200]
|
|
198
|
+
|
|
199
|
+
|
|
184
200
|
def post_event(
|
|
185
201
|
tool_name: str,
|
|
186
202
|
tool_input: dict,
|
|
@@ -207,7 +223,7 @@ def post_event(
|
|
|
207
223
|
"user_email": cfg.get("user_email"),
|
|
208
224
|
"ai_tool": detect_ai_tool(),
|
|
209
225
|
"tool_call": tool_name,
|
|
210
|
-
"input_summary":
|
|
226
|
+
"input_summary": _safe_summary(tool_input),
|
|
211
227
|
"decision": decision,
|
|
212
228
|
"rule_id": rule_id,
|
|
213
229
|
"rule_message": message,
|
|
@@ -57,12 +57,90 @@ def _check_proxy_token() -> None:
|
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _backfill_prev_session(current_session_id: str, cwd: str | None) -> None:
|
|
61
|
+
"""Parse + PATCH the most recent completed session (the one that just ended).
|
|
62
|
+
|
|
63
|
+
Runs silently — never raises, never blocks the new session.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
import re, urllib.request
|
|
67
|
+
from pathlib import Path as _P
|
|
68
|
+
|
|
69
|
+
claude_projects = _P.home() / ".claude" / "projects"
|
|
70
|
+
if not claude_projects.exists():
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# find the project slug dir for this cwd
|
|
74
|
+
if cwd:
|
|
75
|
+
slug = re.sub(r"[^a-zA-Z0-9]", "-", cwd).strip("-")
|
|
76
|
+
project_dir = claude_projects / slug
|
|
77
|
+
else:
|
|
78
|
+
# fall back: pick most recently modified project dir
|
|
79
|
+
dirs = sorted(claude_projects.iterdir(), key=lambda d: d.stat().st_mtime, reverse=True)
|
|
80
|
+
project_dir = dirs[0] if dirs else None
|
|
81
|
+
|
|
82
|
+
if not project_dir or not project_dir.exists():
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# pick the latest JSONL that isn't the current session
|
|
86
|
+
candidates = sorted(
|
|
87
|
+
(f for f in project_dir.glob("*.jsonl") if f.stem != current_session_id),
|
|
88
|
+
key=lambda f: f.stat().st_mtime,
|
|
89
|
+
reverse=True,
|
|
90
|
+
)
|
|
91
|
+
if not candidates:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
prev_session_id = candidates[0].stem
|
|
95
|
+
|
|
96
|
+
from conduct_cli.hooks.session_parser import parse_session
|
|
97
|
+
data = parse_session("claude_code", prev_session_id, cwd)
|
|
98
|
+
|
|
99
|
+
cfg_path = _P.home() / ".conduct" / "config.json"
|
|
100
|
+
if not cfg_path.exists():
|
|
101
|
+
return
|
|
102
|
+
cfg = json.loads(cfg_path.read_text())
|
|
103
|
+
server = cfg.get("server", "").rstrip("/")
|
|
104
|
+
api_key = cfg.get("api_key", "")
|
|
105
|
+
workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
|
|
106
|
+
if not server:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
payload = json.dumps({
|
|
110
|
+
"intent": data.intent,
|
|
111
|
+
"tool_sequence": data.tool_sequence,
|
|
112
|
+
"session_parse_status": data.status,
|
|
113
|
+
"session_parser": data.parser,
|
|
114
|
+
}).encode()
|
|
115
|
+
req = urllib.request.Request(
|
|
116
|
+
f"{server}/guard/sessions/{prev_session_id}/intent",
|
|
117
|
+
data=payload,
|
|
118
|
+
headers={
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"X-API-Key": api_key,
|
|
121
|
+
"X-Workspace-ID": workspace_id,
|
|
122
|
+
},
|
|
123
|
+
method="PATCH",
|
|
124
|
+
)
|
|
125
|
+
urllib.request.urlopen(req, timeout=8)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
60
130
|
def main() -> None:
|
|
131
|
+
current_session_id = ""
|
|
132
|
+
cwd = None
|
|
61
133
|
try:
|
|
62
|
-
sys.stdin.read()
|
|
134
|
+
raw = sys.stdin.read()
|
|
135
|
+
hook_data = json.loads(raw) if raw.strip() else {}
|
|
136
|
+
current_session_id = hook_data.get("session_id", "")
|
|
137
|
+
cwd = hook_data.get("cwd")
|
|
63
138
|
except Exception:
|
|
64
139
|
pass
|
|
65
140
|
|
|
141
|
+
if current_session_id:
|
|
142
|
+
_backfill_prev_session(current_session_id, cwd)
|
|
143
|
+
|
|
66
144
|
_check_proxy_token()
|
|
67
145
|
|
|
68
146
|
if not SNAPSHOT_PATH.exists():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|