conduct-cli 0.4.67__tar.gz → 0.4.68__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.4.67 → conduct_cli-0.4.68}/PKG-INFO +1 -1
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/pyproject.toml +1 -1
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/guardmcp.py +2 -2
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/hook_precompact_template.py +7 -3
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/main.py +109 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/README.md +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/setup.cfg +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/setup.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.68}/tests/test_switch.py +0 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""
|
|
3
3
|
conductguard-mcp — ConductGuard MCP server.
|
|
4
4
|
|
|
5
|
-
Runs as a subprocess started by Claude Code / Cursor / Windsurf via
|
|
6
|
-
mcpServers config written by `conduct guard sync`. Communicates over
|
|
5
|
+
Runs as a subprocess started by Claude Code / Codex / Cursor / Windsurf via
|
|
6
|
+
the mcpServers config written by `conduct guard sync`. Communicates over
|
|
7
7
|
stdin/stdout using JSON-RPC 2.0 (MCP stdio transport).
|
|
8
8
|
|
|
9
9
|
Exposes three tools:
|
|
@@ -35,9 +35,13 @@ def _memory_headline():
|
|
|
35
35
|
try:
|
|
36
36
|
root = Path.cwd()
|
|
37
37
|
mem_key = str(root).replace("/", "-").lstrip("-")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
candidates = [
|
|
39
|
+
Path.home() / ".claude" / "projects" / mem_key / "memory" / "MEMORY.md",
|
|
40
|
+
Path.home() / ".codex" / "projects" / mem_key / "memory" / "MEMORY.md",
|
|
41
|
+
]
|
|
42
|
+
for mem_path in candidates:
|
|
43
|
+
if mem_path.exists():
|
|
44
|
+
return "\n".join(mem_path.read_text().splitlines()[:10])
|
|
41
45
|
except Exception:
|
|
42
46
|
pass
|
|
43
47
|
return ""
|
|
@@ -2126,6 +2126,111 @@ def classify_finding(text: str) -> "dict | None":
|
|
|
2126
2126
|
return None
|
|
2127
2127
|
|
|
2128
2128
|
|
|
2129
|
+
def cmd_session_report(args):
|
|
2130
|
+
"""
|
|
2131
|
+
Run paxel-local against local Claude Code transcripts and send a
|
|
2132
|
+
developer profile report to the workspace admin via the Conduct API.
|
|
2133
|
+
"""
|
|
2134
|
+
import json as _json
|
|
2135
|
+
import shutil
|
|
2136
|
+
import subprocess
|
|
2137
|
+
import tempfile
|
|
2138
|
+
import urllib.request
|
|
2139
|
+
|
|
2140
|
+
server, workspace_id, api_key, token = _require_auth(args)
|
|
2141
|
+
hdrs = api.headers(workspace_id, token, "application/json", api_key)
|
|
2142
|
+
|
|
2143
|
+
# ── 1. Fetch paxel-local ─────────────────────────────────────────────────
|
|
2144
|
+
PAXEL_URL = "https://raw.githubusercontent.com/Photobombastic/paxel-local/main/paxel.py"
|
|
2145
|
+
tmpdir = tempfile.mkdtemp(prefix="conduct-paxel-")
|
|
2146
|
+
paxel_script = Path(tmpdir) / "paxel.py"
|
|
2147
|
+
|
|
2148
|
+
print("Downloading paxel-local…")
|
|
2149
|
+
try:
|
|
2150
|
+
urllib.request.urlretrieve(PAXEL_URL, paxel_script)
|
|
2151
|
+
except Exception as exc:
|
|
2152
|
+
print(f"ERROR: could not download paxel-local: {exc}")
|
|
2153
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2154
|
+
sys.exit(1)
|
|
2155
|
+
|
|
2156
|
+
# ── 2. Run paxel ─────────────────────────────────────────────────────────
|
|
2157
|
+
print("Analysing sessions…")
|
|
2158
|
+
try:
|
|
2159
|
+
result = subprocess.run(
|
|
2160
|
+
[sys.executable, str(paxel_script), "--no-open"],
|
|
2161
|
+
cwd=tmpdir,
|
|
2162
|
+
capture_output=True,
|
|
2163
|
+
text=True,
|
|
2164
|
+
timeout=120,
|
|
2165
|
+
)
|
|
2166
|
+
except subprocess.TimeoutExpired:
|
|
2167
|
+
print("ERROR: paxel analysis timed out after 120 s.")
|
|
2168
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2169
|
+
sys.exit(1)
|
|
2170
|
+
|
|
2171
|
+
stats_path = Path(tmpdir) / "stats.json"
|
|
2172
|
+
report_path = Path(tmpdir) / "report.md"
|
|
2173
|
+
|
|
2174
|
+
if not stats_path.exists():
|
|
2175
|
+
print("ERROR: paxel did not produce stats.json — no transcripts found?")
|
|
2176
|
+
if result.stderr:
|
|
2177
|
+
print(result.stderr[:500])
|
|
2178
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2179
|
+
sys.exit(1)
|
|
2180
|
+
|
|
2181
|
+
with open(stats_path) as f:
|
|
2182
|
+
stats = _json.load(f)
|
|
2183
|
+
|
|
2184
|
+
report_md = report_path.read_text() if report_path.exists() else ""
|
|
2185
|
+
|
|
2186
|
+
# ── 3. Build summary payload ─────────────────────────────────────────────
|
|
2187
|
+
import getpass, socket
|
|
2188
|
+
developer = getattr(args, "developer", None) or getpass.getuser()
|
|
2189
|
+
hostname = socket.gethostname()
|
|
2190
|
+
|
|
2191
|
+
archetype = stats.get("archetype", {})
|
|
2192
|
+
competency = stats.get("competency_scores", stats.get("competencies", {}))
|
|
2193
|
+
sessions = stats.get("total_sessions", stats.get("session_count", "?"))
|
|
2194
|
+
tools_used = stats.get("tools_detected", stats.get("tools", []))
|
|
2195
|
+
|
|
2196
|
+
payload = {
|
|
2197
|
+
"developer": developer,
|
|
2198
|
+
"hostname": hostname,
|
|
2199
|
+
"archetype": archetype.get("name", archetype) if isinstance(archetype, dict) else str(archetype),
|
|
2200
|
+
"archetype_description": archetype.get("description", "") if isinstance(archetype, dict) else "",
|
|
2201
|
+
"competency_scores": competency,
|
|
2202
|
+
"total_sessions": sessions,
|
|
2203
|
+
"tools_detected": tools_used,
|
|
2204
|
+
"report_md": report_md[:4000], # cap to avoid huge payloads
|
|
2205
|
+
"raw_stats": stats,
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
# ── 4. Send to Conduct API ────────────────────────────────────────────────
|
|
2209
|
+
print("Sending report to admin…")
|
|
2210
|
+
resp = api.post(
|
|
2211
|
+
server,
|
|
2212
|
+
"/session-reports",
|
|
2213
|
+
payload,
|
|
2214
|
+
hdrs,
|
|
2215
|
+
)
|
|
2216
|
+
|
|
2217
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2218
|
+
|
|
2219
|
+
if resp and resp.get("id"):
|
|
2220
|
+
arch = payload["archetype"]
|
|
2221
|
+
exec_score = competency.get("execution", competency.get("Execution", "?"))
|
|
2222
|
+
plan_score = competency.get("planning", competency.get("Planning", "?"))
|
|
2223
|
+
eng_score = competency.get("engineering", competency.get("Engineering", "?"))
|
|
2224
|
+
print(f"\nReport sent.")
|
|
2225
|
+
print(f" Archetype : {arch}")
|
|
2226
|
+
print(f" Execution : {exec_score} Planning: {plan_score} Engineering: {eng_score}")
|
|
2227
|
+
print(f" Sessions : {sessions}")
|
|
2228
|
+
if tools_used:
|
|
2229
|
+
print(f" Tools : {', '.join(tools_used) if isinstance(tools_used, list) else tools_used}")
|
|
2230
|
+
else:
|
|
2231
|
+
print(f"WARNING: server response unexpected: {resp}")
|
|
2232
|
+
|
|
2233
|
+
|
|
2129
2234
|
def cmd_emit_finding(args):
|
|
2130
2235
|
"""POST a security finding to /security-findings."""
|
|
2131
2236
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
@@ -2730,6 +2835,8 @@ def main():
|
|
|
2730
2835
|
sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
|
|
2731
2836
|
sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
|
|
2732
2837
|
sub.add_parser("test-security-verify", help="Post test findings and verify full triage pipeline end-to-end")
|
|
2838
|
+
sr_p = sub.add_parser("session-report", help="Analyse local AI coding sessions with paxel and send report to admin")
|
|
2839
|
+
sr_p.add_argument("--developer", default=None, help="Developer name (defaults to OS username)")
|
|
2733
2840
|
|
|
2734
2841
|
args = parser.parse_args()
|
|
2735
2842
|
|
|
@@ -2806,6 +2913,8 @@ def main():
|
|
|
2806
2913
|
cmd_test_security(args)
|
|
2807
2914
|
elif args.command == "test-security-verify":
|
|
2808
2915
|
cmd_test_security_verify(args)
|
|
2916
|
+
elif args.command == "session-report":
|
|
2917
|
+
cmd_session_report(args)
|
|
2809
2918
|
else:
|
|
2810
2919
|
parser.print_help()
|
|
2811
2920
|
|
|
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
|