conduct-cli 0.4.14__tar.gz → 0.4.16__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.14 → conduct_cli-0.4.16}/PKG-INFO +1 -1
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/pyproject.toml +1 -1
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli/guard.py +110 -43
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/README.md +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/setup.cfg +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/setup.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.16}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -55,11 +55,14 @@ def _fetch_budget_status():
|
|
|
55
55
|
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
56
56
|
except Exception:
|
|
57
57
|
return False, None
|
|
58
|
-
workspace_id
|
|
59
|
-
|
|
58
|
+
workspace_id = cfg.get("workspace_id")
|
|
59
|
+
clerk_user_id = cfg.get("clerk_user_id") or ""
|
|
60
|
+
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
60
61
|
if not workspace_id:
|
|
61
62
|
return False, None
|
|
62
63
|
url = f"{api_url}/guard/spend/budget-check?workspace_id={workspace_id}"
|
|
64
|
+
if clerk_user_id:
|
|
65
|
+
url += f"&clerk_user_id={clerk_user_id}"
|
|
63
66
|
try:
|
|
64
67
|
with urllib.request.urlopen(urllib.request.Request(url), timeout=5) as resp:
|
|
65
68
|
data = json.loads(resp.read())
|
|
@@ -249,54 +252,111 @@ def _read_tokens_from_transcript(transcript_path, tool_use_id):
|
|
|
249
252
|
return 0, 0
|
|
250
253
|
|
|
251
254
|
|
|
252
|
-
def
|
|
253
|
-
"""
|
|
255
|
+
def _scan_codex_tokens(transcript_path):
|
|
256
|
+
"""Robustly scan a Codex transcript for the last token_count event.
|
|
257
|
+
|
|
258
|
+
Reads in 512 KB chunks from the end so it handles arbitrarily large
|
|
259
|
+
tool-output lines without a fixed cutoff.
|
|
260
|
+
"""
|
|
254
261
|
try:
|
|
255
262
|
path = Path(transcript_path)
|
|
256
263
|
if not path.exists():
|
|
257
264
|
return 0, 0
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
265
|
+
size = path.stat().st_size
|
|
266
|
+
chunk_size = 524288 # 512 KB
|
|
267
|
+
buf = b""
|
|
268
|
+
pos = size
|
|
269
|
+
while pos >= 0:
|
|
270
|
+
read_size = min(chunk_size, pos)
|
|
271
|
+
pos -= read_size
|
|
272
|
+
with open(path, "rb") as f:
|
|
273
|
+
f.seek(pos)
|
|
274
|
+
buf = f.read(read_size) + buf
|
|
275
|
+
text = buf.decode("utf-8", errors="ignore")
|
|
276
|
+
# Split; if we haven't reached the start the first fragment may be partial
|
|
277
|
+
parts = text.split("\n")
|
|
278
|
+
start = 1 if pos > 0 else 0
|
|
279
|
+
for line in reversed(parts[start:]):
|
|
280
|
+
if "token_count" not in line or not line.strip():
|
|
281
|
+
continue
|
|
282
|
+
try:
|
|
283
|
+
entry = json.loads(line)
|
|
284
|
+
if entry.get("type") == "event_msg":
|
|
285
|
+
info = entry.get("payload", {}).get("info", {})
|
|
286
|
+
usage = info.get("last_token_usage", {})
|
|
287
|
+
if usage:
|
|
288
|
+
total_in = usage.get("input_tokens", 0)
|
|
289
|
+
total_out = (usage.get("output_tokens", 0)
|
|
290
|
+
+ usage.get("reasoning_output_tokens", 0))
|
|
291
|
+
return total_in, total_out
|
|
292
|
+
except Exception:
|
|
293
|
+
continue
|
|
294
|
+
if pos == 0:
|
|
295
|
+
break
|
|
273
296
|
except Exception:
|
|
274
297
|
pass
|
|
275
298
|
return 0, 0
|
|
276
299
|
|
|
277
300
|
|
|
278
|
-
def
|
|
279
|
-
"""
|
|
301
|
+
def post_codex_main():
|
|
302
|
+
"""Delayed Codex token reader — spawned as background by post_usage_main.
|
|
303
|
+
|
|
304
|
+
Reads args from a pending JSON file, sleeps 2 s to let Codex flush the
|
|
305
|
+
token_count event, then scans the transcript and POSTs to the API.
|
|
306
|
+
"""
|
|
307
|
+
import time
|
|
308
|
+
if len(sys.argv) < 3:
|
|
309
|
+
sys.exit(0)
|
|
310
|
+
pending_path = Path(sys.argv[2])
|
|
280
311
|
try:
|
|
281
|
-
|
|
312
|
+
args = json.loads(pending_path.read_text())
|
|
313
|
+
pending_path.unlink(missing_ok=True)
|
|
282
314
|
except Exception:
|
|
283
315
|
sys.exit(0)
|
|
316
|
+
time.sleep(2)
|
|
317
|
+
transcript_path = args.get("transcript_path", "")
|
|
318
|
+
tokens_in, tokens_out = _scan_codex_tokens(transcript_path)
|
|
319
|
+
if tokens_in or tokens_out:
|
|
320
|
+
_post_usage(args.get("session_id"), args.get("tool_name"),
|
|
321
|
+
tokens_in, tokens_out, None)
|
|
322
|
+
sys.exit(0)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def post_usage_main():
|
|
326
|
+
"""PostToolUse hook entrypoint — exits immediately; heavy work is async."""
|
|
284
327
|
try:
|
|
285
|
-
|
|
328
|
+
data = json.load(sys.stdin)
|
|
286
329
|
except Exception:
|
|
287
|
-
|
|
330
|
+
sys.exit(0)
|
|
288
331
|
session_id = data.get("session_id")
|
|
289
332
|
tool_name = (data.get("tool_name") or "").lower()
|
|
290
333
|
tool_use_id = data.get("tool_use_id")
|
|
291
334
|
transcript_path = data.get("transcript_path")
|
|
292
335
|
is_codex = (tool_use_id or "").startswith("call_")
|
|
293
|
-
|
|
294
|
-
|
|
336
|
+
|
|
337
|
+
if is_codex and transcript_path:
|
|
338
|
+
# Write pending args; spawn delayed background reader so hook exits instantly
|
|
339
|
+
import uuid as _uuid
|
|
340
|
+
pending = GUARD_DIR / f"codex_pending_{_uuid.uuid4().hex[:8]}.json"
|
|
341
|
+
try:
|
|
342
|
+
pending.write_text(json.dumps({
|
|
343
|
+
"session_id": session_id,
|
|
344
|
+
"tool_name": tool_name,
|
|
345
|
+
"transcript_path": transcript_path,
|
|
346
|
+
}))
|
|
347
|
+
hook_path = Path(__file__).resolve()
|
|
348
|
+
subprocess.Popen(
|
|
349
|
+
[sys.executable, str(hook_path), "post-codex", str(pending)],
|
|
350
|
+
stdout=subprocess.DEVNULL,
|
|
351
|
+
stderr=subprocess.DEVNULL,
|
|
352
|
+
start_new_session=True,
|
|
353
|
+
)
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
295
356
|
elif transcript_path:
|
|
296
357
|
tokens_input, tokens_output = _read_tokens_from_transcript(transcript_path, tool_use_id)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
_post_usage(session_id, tool_name, tokens_input, tokens_output, None)
|
|
358
|
+
_post_usage(session_id, tool_name, tokens_input, tokens_output, None)
|
|
359
|
+
|
|
300
360
|
sys.exit(0)
|
|
301
361
|
|
|
302
362
|
|
|
@@ -336,6 +396,8 @@ def main():
|
|
|
336
396
|
if __name__ == "__main__":
|
|
337
397
|
if len(sys.argv) > 1 and sys.argv[1] == "post":
|
|
338
398
|
post_usage_main()
|
|
399
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "post-codex":
|
|
400
|
+
post_codex_main()
|
|
339
401
|
else:
|
|
340
402
|
main()
|
|
341
403
|
'''
|
|
@@ -607,16 +669,18 @@ def cmd_guard_install(args):
|
|
|
607
669
|
print(f" {GRAY}Guard not installed for this workspace — skipping{RESET}")
|
|
608
670
|
return
|
|
609
671
|
|
|
610
|
-
member_token
|
|
611
|
-
user_email
|
|
672
|
+
member_token = result.get("member_token") or ""
|
|
673
|
+
user_email = result.get("user_email") or ""
|
|
674
|
+
clerk_user_id = result.get("clerk_user_id") or ""
|
|
612
675
|
|
|
613
676
|
# Persist guard config — include api_key so CLI commands can authenticate
|
|
614
677
|
_save_guard_config({
|
|
615
|
-
"workspace_id":
|
|
616
|
-
"member_token":
|
|
617
|
-
"user_email":
|
|
618
|
-
"
|
|
619
|
-
"
|
|
678
|
+
"workspace_id": workspace_id,
|
|
679
|
+
"member_token": member_token,
|
|
680
|
+
"user_email": user_email,
|
|
681
|
+
"clerk_user_id": clerk_user_id,
|
|
682
|
+
"api_key": api_key,
|
|
683
|
+
"api_url": server,
|
|
620
684
|
})
|
|
621
685
|
|
|
622
686
|
# Download policies
|
|
@@ -735,19 +799,22 @@ def cmd_guard_status(args):
|
|
|
735
799
|
api_key = cfg.get("api_key", "")
|
|
736
800
|
base_url = _api_url(cfg)
|
|
737
801
|
|
|
738
|
-
# Auto-refresh user_email into config if
|
|
739
|
-
if not user_email and api_key:
|
|
802
|
+
# Auto-refresh user_email + clerk_user_id into config if missing
|
|
803
|
+
if (not user_email or not cfg.get("clerk_user_id")) and api_key:
|
|
740
804
|
try:
|
|
741
805
|
installed = _req("GET", f"{base_url}/guard/config/installed", api_key=api_key)
|
|
742
806
|
fetched_email = installed.get("user_email") or ""
|
|
807
|
+
fetched_clerk = installed.get("clerk_user_id") or ""
|
|
743
808
|
if fetched_email:
|
|
744
809
|
cfg["user_email"] = fetched_email
|
|
745
|
-
_save_guard_config(cfg)
|
|
746
|
-
# Rewrite hook script so future events carry the email
|
|
747
|
-
hook_path = GUARD_DIR / "hook.py"
|
|
748
|
-
hook_path.write_text(_HOOK_SCRIPT)
|
|
749
|
-
hook_path.chmod(0o755)
|
|
750
810
|
user_email = fetched_email
|
|
811
|
+
if fetched_clerk:
|
|
812
|
+
cfg["clerk_user_id"] = fetched_clerk
|
|
813
|
+
_save_guard_config(cfg)
|
|
814
|
+
# Rewrite hook script so future events carry the email
|
|
815
|
+
hook_path = GUARD_DIR / "hook.py"
|
|
816
|
+
hook_path.write_text(_HOOK_SCRIPT)
|
|
817
|
+
hook_path.chmod(0o755)
|
|
751
818
|
except Exception:
|
|
752
819
|
pass
|
|
753
820
|
|
|
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
|