conduct-cli 0.4.67__tar.gz → 0.4.69__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.69}/PKG-INFO +1 -1
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/pyproject.toml +1 -1
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/guardmcp.py +2 -2
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/hook_precompact_template.py +7 -3
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/main.py +130 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/README.md +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/setup.cfg +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/setup.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.67 → conduct_cli-0.4.69}/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,132 @@ 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
|
+
volume = stats.get("volume", {})
|
|
2192
|
+
behavior = stats.get("behavior", {})
|
|
2193
|
+
autonomy = stats.get("autonomy", {})
|
|
2194
|
+
tools = stats.get("tools", {})
|
|
2195
|
+
velocity = stats.get("velocity", {})
|
|
2196
|
+
|
|
2197
|
+
sessions = volume.get("total_sessions", "?")
|
|
2198
|
+
prompts = volume.get("total_prompts", "?")
|
|
2199
|
+
autonomy_score = autonomy.get("autonomy_score_0_100", "?")
|
|
2200
|
+
top_tools = [t[0] for t in (tools.get("top_tools") or [])[:5]]
|
|
2201
|
+
commits = velocity.get("git_commits_real", "?") if isinstance(velocity, dict) else "?"
|
|
2202
|
+
|
|
2203
|
+
# Derive a simple archetype label from the data
|
|
2204
|
+
planning_ratio = behavior.get("planning_ratio_explore_to_doing", 0)
|
|
2205
|
+
if autonomy_score != "?" and float(autonomy_score) >= 70:
|
|
2206
|
+
archetype = "Autonomous Builder"
|
|
2207
|
+
elif planning_ratio != 0 and float(planning_ratio) > 1.0:
|
|
2208
|
+
archetype = "Strategic Planner"
|
|
2209
|
+
else:
|
|
2210
|
+
archetype = "Execution-Focused Builder"
|
|
2211
|
+
|
|
2212
|
+
payload = {
|
|
2213
|
+
"developer": developer,
|
|
2214
|
+
"hostname": hostname,
|
|
2215
|
+
"archetype": archetype,
|
|
2216
|
+
"competency_scores": {
|
|
2217
|
+
"autonomy": autonomy_score,
|
|
2218
|
+
"planning_ratio": planning_ratio,
|
|
2219
|
+
"commits": commits,
|
|
2220
|
+
},
|
|
2221
|
+
"total_sessions": sessions,
|
|
2222
|
+
"tools_detected": top_tools,
|
|
2223
|
+
"report_md": report_md[:4000],
|
|
2224
|
+
"raw_stats": {
|
|
2225
|
+
"volume": volume,
|
|
2226
|
+
"autonomy": autonomy,
|
|
2227
|
+
"top_tools": top_tools,
|
|
2228
|
+
"velocity": velocity,
|
|
2229
|
+
"scope": stats.get("scope", ""),
|
|
2230
|
+
},
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
# ── 4. Send to Conduct API ────────────────────────────────────────────────
|
|
2234
|
+
print("Sending report to admin…")
|
|
2235
|
+
resp = api.req(
|
|
2236
|
+
"POST",
|
|
2237
|
+
f"{server}/session-reports",
|
|
2238
|
+
hdrs,
|
|
2239
|
+
body=payload,
|
|
2240
|
+
)
|
|
2241
|
+
|
|
2242
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2243
|
+
|
|
2244
|
+
if resp and resp.get("id"):
|
|
2245
|
+
print(f"\nReport sent.")
|
|
2246
|
+
print(f" Archetype : {archetype}")
|
|
2247
|
+
print(f" Autonomy : {autonomy_score}/100 Planning ratio: {planning_ratio}")
|
|
2248
|
+
print(f" Sessions : {sessions} Prompts: {prompts} Commits: {commits}")
|
|
2249
|
+
if top_tools:
|
|
2250
|
+
print(f" Top tools : {', '.join(top_tools)}")
|
|
2251
|
+
else:
|
|
2252
|
+
print(f"WARNING: server response unexpected: {resp}")
|
|
2253
|
+
|
|
2254
|
+
|
|
2129
2255
|
def cmd_emit_finding(args):
|
|
2130
2256
|
"""POST a security finding to /security-findings."""
|
|
2131
2257
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
@@ -2730,6 +2856,8 @@ def main():
|
|
|
2730
2856
|
sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
|
|
2731
2857
|
sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
|
|
2732
2858
|
sub.add_parser("test-security-verify", help="Post test findings and verify full triage pipeline end-to-end")
|
|
2859
|
+
sr_p = sub.add_parser("session-report", help="Analyse local AI coding sessions with paxel and send report to admin")
|
|
2860
|
+
sr_p.add_argument("--developer", default=None, help="Developer name (defaults to OS username)")
|
|
2733
2861
|
|
|
2734
2862
|
args = parser.parse_args()
|
|
2735
2863
|
|
|
@@ -2806,6 +2934,8 @@ def main():
|
|
|
2806
2934
|
cmd_test_security(args)
|
|
2807
2935
|
elif args.command == "test-security-verify":
|
|
2808
2936
|
cmd_test_security_verify(args)
|
|
2937
|
+
elif args.command == "session-report":
|
|
2938
|
+
cmd_session_report(args)
|
|
2809
2939
|
else:
|
|
2810
2940
|
parser.print_help()
|
|
2811
2941
|
|
|
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
|