conduct-cli 0.4.4__tar.gz → 0.4.6__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.4 → conduct_cli-0.4.6}/PKG-INFO +1 -1
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/pyproject.toml +2 -1
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli/guard.py +139 -21
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli.egg-info/entry_points.txt +1 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/README.md +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/setup.cfg +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/setup.py +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.4 → conduct_cli-0.4.6}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.6"
|
|
8
8
|
description = "CLI for Conduct AI — install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -33,6 +33,7 @@ Repository = "https://github.com/sseshachala/conductai"
|
|
|
33
33
|
[project.scripts]
|
|
34
34
|
conduct = "conduct_cli.main:main"
|
|
35
35
|
conductguard-mcp = "conduct_cli.guardmcp:main"
|
|
36
|
+
conductguard-post = "conduct_cli.guard:post_usage_main"
|
|
36
37
|
|
|
37
38
|
[tool.setuptools.packages.find]
|
|
38
39
|
where = ["src"]
|
|
@@ -125,7 +125,7 @@ def _detect_ai_tool():
|
|
|
125
125
|
return "unknown"
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def _post_event(tool_name, tool_input, decision, rule_id=None, message=None):
|
|
128
|
+
def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, tool_use_id=None):
|
|
129
129
|
try:
|
|
130
130
|
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
131
131
|
except Exception:
|
|
@@ -144,6 +144,7 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None):
|
|
|
144
144
|
"decision": decision,
|
|
145
145
|
"rule_id": rule_id,
|
|
146
146
|
"rule_message": message,
|
|
147
|
+
"tool_use_id": tool_use_id,
|
|
147
148
|
})
|
|
148
149
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
149
150
|
script = (
|
|
@@ -162,6 +163,55 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None):
|
|
|
162
163
|
)
|
|
163
164
|
|
|
164
165
|
|
|
166
|
+
def _post_usage(tool_use_id, tokens_input, tokens_output, duration_ms, ai_tool):
|
|
167
|
+
"""Fire-and-forget POST to /guard/events/usage"""
|
|
168
|
+
try:
|
|
169
|
+
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
170
|
+
except Exception:
|
|
171
|
+
return
|
|
172
|
+
workspace_id = cfg.get("workspace_id")
|
|
173
|
+
if not workspace_id or not tool_use_id:
|
|
174
|
+
return
|
|
175
|
+
payload = json.dumps({
|
|
176
|
+
"workspace_id": workspace_id,
|
|
177
|
+
"tool_use_id": tool_use_id,
|
|
178
|
+
"tokens_input": tokens_input,
|
|
179
|
+
"tokens_output": tokens_output,
|
|
180
|
+
"duration_ms": duration_ms,
|
|
181
|
+
"ai_tool": _detect_ai_tool(),
|
|
182
|
+
})
|
|
183
|
+
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
184
|
+
script = (
|
|
185
|
+
"import urllib.request\\n"
|
|
186
|
+
"try:\\n"
|
|
187
|
+
f" req = urllib.request.Request(\\"{api_url}/guard/events/usage\\","
|
|
188
|
+
f" data={repr(payload.encode())}, headers={{\\\"Content-Type\\\": \\\"application/json\\\"}}, method=\\"POST\\")\\n"
|
|
189
|
+
" urllib.request.urlopen(req, timeout=5)\\n"
|
|
190
|
+
"except: pass\\n"
|
|
191
|
+
)
|
|
192
|
+
subprocess.Popen(
|
|
193
|
+
[sys.executable, "-c", script],
|
|
194
|
+
stdout=subprocess.DEVNULL,
|
|
195
|
+
stderr=subprocess.DEVNULL,
|
|
196
|
+
start_new_session=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def post_usage_main():
|
|
201
|
+
"""PostToolUse hook entrypoint — captures token usage and duration."""
|
|
202
|
+
try:
|
|
203
|
+
data = json.load(sys.stdin)
|
|
204
|
+
except Exception:
|
|
205
|
+
sys.exit(0)
|
|
206
|
+
tool_use_id = data.get("tool_use_id")
|
|
207
|
+
usage = data.get("usage") or {}
|
|
208
|
+
tokens_input = usage.get("input_tokens", 0)
|
|
209
|
+
tokens_output = usage.get("output_tokens", 0)
|
|
210
|
+
duration_ms = data.get("duration_ms")
|
|
211
|
+
_post_usage(tool_use_id, tokens_input, tokens_output, duration_ms, _detect_ai_tool())
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
|
|
165
215
|
def main():
|
|
166
216
|
try:
|
|
167
217
|
data = json.load(sys.stdin)
|
|
@@ -178,12 +228,13 @@ def main():
|
|
|
178
228
|
|
|
179
229
|
tool_name = (data.get("tool_name") or "").lower()
|
|
180
230
|
tool_input = data.get("tool_input") or {}
|
|
231
|
+
tool_use_id = data.get("tool_use_id")
|
|
181
232
|
|
|
182
233
|
_, action, rule_id, message = _check_policy(tool_name, tool_input)
|
|
183
234
|
|
|
184
235
|
# Always post an event — "allowed" for normal calls, "blocked"/"warned" for violations
|
|
185
236
|
decision = {"block": "blocked", "warn": "warned", "approval": "blocked"}.get(action, "allowed")
|
|
186
|
-
_post_event(tool_name, tool_input, decision, rule_id, message)
|
|
237
|
+
_post_event(tool_name, tool_input, decision, rule_id, message, tool_use_id=tool_use_id)
|
|
187
238
|
|
|
188
239
|
if action == "block":
|
|
189
240
|
print(f"[ConductGuard] {message}")
|
|
@@ -195,7 +246,10 @@ def main():
|
|
|
195
246
|
|
|
196
247
|
|
|
197
248
|
if __name__ == "__main__":
|
|
198
|
-
|
|
249
|
+
if len(sys.argv) > 1 and sys.argv[1] == "post":
|
|
250
|
+
post_usage_main()
|
|
251
|
+
else:
|
|
252
|
+
main()
|
|
199
253
|
'''
|
|
200
254
|
|
|
201
255
|
# ── Guard config helpers ──────────────────────────────────────────────────────
|
|
@@ -265,7 +319,7 @@ def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
|
|
|
265
319
|
|
|
266
320
|
|
|
267
321
|
def _install_codex_hook(hook_path: Path) -> None:
|
|
268
|
-
"""Register
|
|
322
|
+
"""Register PreToolUse and PostToolUse hooks in ~/.codex/hooks.json."""
|
|
269
323
|
codex_hooks = Path.home() / ".codex" / "hooks.json"
|
|
270
324
|
if not (Path.home() / ".codex").exists():
|
|
271
325
|
return # Codex not installed
|
|
@@ -277,20 +331,53 @@ def _install_codex_hook(hook_path: Path) -> None:
|
|
|
277
331
|
except json.JSONDecodeError:
|
|
278
332
|
hooks = {}
|
|
279
333
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
334
|
+
hook_section = hooks.setdefault("hooks", {})
|
|
335
|
+
|
|
336
|
+
# PreToolUse
|
|
337
|
+
pre_cmd = f"python3 {hook_path}"
|
|
338
|
+
pre = hook_section.setdefault("PreToolUse", [])
|
|
339
|
+
pre_already = any(
|
|
340
|
+
e.get("command") == pre_cmd
|
|
284
341
|
for h in pre
|
|
285
342
|
for e in h.get("hooks", [])
|
|
286
343
|
)
|
|
287
|
-
|
|
288
|
-
|
|
344
|
+
changed = False
|
|
345
|
+
if not pre_already:
|
|
346
|
+
pre.append({"matcher": ".*", "hooks": [{"type": "command", "command": pre_cmd}]})
|
|
347
|
+
changed = True
|
|
348
|
+
|
|
349
|
+
# PostToolUse — self-contained: python3 /path/hook.py post (no PATH dependency)
|
|
350
|
+
post_cmd = f"python3 {hook_path} post"
|
|
351
|
+
post = hook_section.setdefault("PostToolUse", [])
|
|
352
|
+
# Remove stale conductguard-post entries registered by older CLI versions
|
|
353
|
+
stale = "conductguard-post"
|
|
354
|
+
cleaned = False
|
|
355
|
+
for h in post:
|
|
356
|
+
before = len(h.get("hooks", []))
|
|
357
|
+
h["hooks"] = [e for e in h.get("hooks", []) if e.get("command") != stale]
|
|
358
|
+
if len(h["hooks"]) < before:
|
|
359
|
+
cleaned = True
|
|
360
|
+
post[:] = [h for h in post if h.get("hooks")]
|
|
361
|
+
post_already = any(
|
|
362
|
+
e.get("command") == post_cmd
|
|
363
|
+
for h in post
|
|
364
|
+
for e in h.get("hooks", [])
|
|
365
|
+
)
|
|
366
|
+
if not post_already:
|
|
367
|
+
post.append({"matcher": ".*", "hooks": [{"type": "command", "command": post_cmd}]})
|
|
368
|
+
changed = True
|
|
369
|
+
if cleaned:
|
|
370
|
+
changed = True
|
|
371
|
+
|
|
372
|
+
if changed:
|
|
289
373
|
codex_hooks.parent.mkdir(parents=True, exist_ok=True)
|
|
290
374
|
codex_hooks.write_text(json.dumps(hooks, indent=2))
|
|
291
|
-
|
|
375
|
+
if not pre_already:
|
|
376
|
+
print(f" {GREEN}Codex PreToolUse hook registered{RESET}")
|
|
377
|
+
if not post_already or cleaned:
|
|
378
|
+
print(f" {GREEN}Codex PostToolUse hook registered{RESET}")
|
|
292
379
|
else:
|
|
293
|
-
print(f" {GRAY}Codex
|
|
380
|
+
print(f" {GRAY}Codex hooks already registered{RESET}")
|
|
294
381
|
|
|
295
382
|
|
|
296
383
|
# ── HTTP helpers (no third-party deps — mirrors api.py style) ─────────────────
|
|
@@ -342,7 +429,7 @@ def _parse_since(since_str: str) -> str:
|
|
|
342
429
|
# ── Hook helpers ─────────────────────────────────────────────────────────────
|
|
343
430
|
|
|
344
431
|
def _install_claude_hook(hook_path: Path) -> None:
|
|
345
|
-
"""Register
|
|
432
|
+
"""Register PreToolUse and PostToolUse hooks in ~/.claude/settings.json."""
|
|
346
433
|
claude_settings = Path.home() / ".claude" / "settings.json"
|
|
347
434
|
settings: dict = {}
|
|
348
435
|
if claude_settings.exists():
|
|
@@ -352,21 +439,52 @@ def _install_claude_hook(hook_path: Path) -> None:
|
|
|
352
439
|
settings = {}
|
|
353
440
|
|
|
354
441
|
hooks = settings.setdefault("hooks", {})
|
|
355
|
-
pre = hooks.setdefault("PreToolUse", [])
|
|
356
|
-
cmd = f"python3 {hook_path}"
|
|
357
442
|
|
|
358
|
-
|
|
359
|
-
|
|
443
|
+
# PreToolUse — existing hook script
|
|
444
|
+
pre = hooks.setdefault("PreToolUse", [])
|
|
445
|
+
pre_cmd = f"python3 {hook_path}"
|
|
446
|
+
pre_already = any(
|
|
447
|
+
e.get("command") == pre_cmd
|
|
360
448
|
for h in pre
|
|
361
449
|
for e in h.get("hooks", [])
|
|
362
450
|
)
|
|
363
|
-
|
|
364
|
-
|
|
451
|
+
changed = False
|
|
452
|
+
if not pre_already:
|
|
453
|
+
pre.append({"matcher": ".*", "hooks": [{"type": "command", "command": pre_cmd}]})
|
|
454
|
+
changed = True
|
|
455
|
+
|
|
456
|
+
# PostToolUse — self-contained: python3 /path/hook.py post (no PATH dependency)
|
|
457
|
+
post = hooks.setdefault("PostToolUse", [])
|
|
458
|
+
post_cmd = f"python3 {hook_path} post"
|
|
459
|
+
# Remove stale conductguard-post entries registered by older CLI versions
|
|
460
|
+
stale = "conductguard-post"
|
|
461
|
+
cleaned = False
|
|
462
|
+
for h in post:
|
|
463
|
+
before = len(h.get("hooks", []))
|
|
464
|
+
h["hooks"] = [e for e in h.get("hooks", []) if e.get("command") != stale]
|
|
465
|
+
if len(h["hooks"]) < before:
|
|
466
|
+
cleaned = True
|
|
467
|
+
post[:] = [h for h in post if h.get("hooks")]
|
|
468
|
+
post_already = any(
|
|
469
|
+
e.get("command") == post_cmd
|
|
470
|
+
for h in post
|
|
471
|
+
for e in h.get("hooks", [])
|
|
472
|
+
)
|
|
473
|
+
if not post_already:
|
|
474
|
+
post.append({"matcher": ".*", "hooks": [{"type": "command", "command": post_cmd}]})
|
|
475
|
+
changed = True
|
|
476
|
+
if cleaned:
|
|
477
|
+
changed = True
|
|
478
|
+
|
|
479
|
+
if changed:
|
|
365
480
|
claude_settings.parent.mkdir(parents=True, exist_ok=True)
|
|
366
481
|
claude_settings.write_text(json.dumps(settings, indent=2))
|
|
367
|
-
|
|
482
|
+
if not pre_already:
|
|
483
|
+
print(f" {GREEN}Claude Code PreToolUse hook registered{RESET}")
|
|
484
|
+
if not post_already or cleaned:
|
|
485
|
+
print(f" {GREEN}Claude Code PostToolUse hook registered{RESET}")
|
|
368
486
|
else:
|
|
369
|
-
print(f" {GRAY}Claude Code
|
|
487
|
+
print(f" {GRAY}Claude Code hooks already registered{RESET}")
|
|
370
488
|
|
|
371
489
|
|
|
372
490
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
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
|