conduct-cli 0.4.66__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.
Files changed (24) hide show
  1. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/PKG-INFO +1 -1
  2. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/pyproject.toml +1 -1
  3. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/guardmcp.py +2 -2
  4. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/hook_precompact_template.py +7 -3
  5. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/main.py +127 -1
  6. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  7. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/README.md +0 -0
  8. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/setup.cfg +0 -0
  9. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/setup.py +0 -0
  10. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/__init__.py +0 -0
  11. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/api.py +0 -0
  12. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/guard.py +0 -0
  13. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/hook_session_start_template.py +0 -0
  14. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/hook_template.py +0 -0
  15. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/mcp_server.py +0 -0
  16. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  17. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  18. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  19. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/requires.txt +0 -0
  20. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/top_level.txt +0 -0
  21. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_guard_policy.py +0 -0
  22. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_guard_savings.py +0 -0
  23. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_hook_syntax.py +0 -0
  24. {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.66
3
+ Version: 0.4.68
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.4.66"
7
+ version = "0.4.68"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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 the
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
- mem_path = Path.home() / ".claude" / "projects" / mem_key / "memory" / "MEMORY.md"
39
- if mem_path.exists():
40
- return "\n".join(mem_path.read_text().splitlines()[:10])
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)
@@ -2344,8 +2449,23 @@ def cmd_test_security_verify(args):
2344
2449
  TIMEOUT = 300 # 15 runs × ~28s / 4 workers ≈ 105s; 300s gives headroom for queue variance
2345
2450
  POLL_SECS = 5
2346
2451
 
2347
- # ── Step 1: post test findings ────────────────────────────────────────
2452
+ # ── Step 0: fresh Security Automation project ─────────────────────────
2348
2453
  print(f"\n{BOLD}▶ conduct test-security-verify{RESET}")
2454
+ print(f" {GRAY}Step 0/3 — refreshing Security Automation project…{RESET}")
2455
+ try:
2456
+ req = urllib.request.Request(
2457
+ f"{api_url}/secure/refresh-automation?workspace_id={workspace_id}",
2458
+ data=b"{}",
2459
+ headers={"Content-Type": "application/json", "X-Api-Key": api_key},
2460
+ method="POST",
2461
+ )
2462
+ with urllib.request.urlopen(req, timeout=15) as resp:
2463
+ _json.loads(resp.read())
2464
+ print(f" {GREEN}✓{RESET} Fresh project created with latest YAML\n")
2465
+ except Exception as e:
2466
+ print(f" {YELLOW}⚠ refresh failed ({e}) — using existing project{RESET}\n")
2467
+
2468
+ # ── Step 1: post test findings ────────────────────────────────────────
2349
2469
  print(f" {GRAY}Step 1/3 — posting {len(_SECURITY_TEST_CASES)} test findings…{RESET}\n")
2350
2470
 
2351
2471
  # Clean previous run
@@ -2400,6 +2520,7 @@ def cmd_test_security_verify(args):
2400
2520
 
2401
2521
  # ── Step 2: poll until all findings move off "open" ───────────────────
2402
2522
  print(f"\n {GRAY}Step 2/3 — waiting for triage pipeline (timeout {TIMEOUT}s)…{RESET}\n")
2523
+
2403
2524
  deadline = _time.time() + TIMEOUT
2404
2525
  final_statuses: dict[str, str] = {}
2405
2526
 
@@ -2435,6 +2556,7 @@ def cmd_test_security_verify(args):
2435
2556
  # ── Step 3: report per-finding results ────────────────────────────────
2436
2557
  print(f"\n {GRAY}Step 3/3 — results{RESET}\n")
2437
2558
 
2559
+
2438
2560
  all_pass = True
2439
2561
  name_by_id = {}
2440
2562
  for i, (name, *_) in enumerate(_SECURITY_TEST_CASES):
@@ -2713,6 +2835,8 @@ def main():
2713
2835
  sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
2714
2836
  sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
2715
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)")
2716
2840
 
2717
2841
  args = parser.parse_args()
2718
2842
 
@@ -2789,6 +2913,8 @@ def main():
2789
2913
  cmd_test_security(args)
2790
2914
  elif args.command == "test-security-verify":
2791
2915
  cmd_test_security_verify(args)
2916
+ elif args.command == "session-report":
2917
+ cmd_session_report(args)
2792
2918
  else:
2793
2919
  parser.print_help()
2794
2920
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.66
3
+ Version: 0.4.68
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
File without changes
File without changes
File without changes