conduct-cli 0.4.13__tar.gz → 0.4.15__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.13 → conduct_cli-0.4.15}/PKG-INFO +1 -1
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/pyproject.toml +1 -1
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli/guard.py +90 -31
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/README.md +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/setup.cfg +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/setup.py +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.13 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -249,54 +249,111 @@ def _read_tokens_from_transcript(transcript_path, tool_use_id):
|
|
|
249
249
|
return 0, 0
|
|
250
250
|
|
|
251
251
|
|
|
252
|
-
def
|
|
253
|
-
"""
|
|
252
|
+
def _scan_codex_tokens(transcript_path):
|
|
253
|
+
"""Robustly scan a Codex transcript for the last token_count event.
|
|
254
|
+
|
|
255
|
+
Reads in 512 KB chunks from the end so it handles arbitrarily large
|
|
256
|
+
tool-output lines without a fixed cutoff.
|
|
257
|
+
"""
|
|
254
258
|
try:
|
|
255
|
-
|
|
256
|
-
if not
|
|
257
|
-
return 0, 0
|
|
258
|
-
files = sorted(sessions_dir.rglob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
259
|
-
if not files:
|
|
259
|
+
path = Path(transcript_path)
|
|
260
|
+
if not path.exists():
|
|
260
261
|
return 0, 0
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
262
|
+
size = path.stat().st_size
|
|
263
|
+
chunk_size = 524288 # 512 KB
|
|
264
|
+
buf = b""
|
|
265
|
+
pos = size
|
|
266
|
+
while pos >= 0:
|
|
267
|
+
read_size = min(chunk_size, pos)
|
|
268
|
+
pos -= read_size
|
|
269
|
+
with open(path, "rb") as f:
|
|
270
|
+
f.seek(pos)
|
|
271
|
+
buf = f.read(read_size) + buf
|
|
272
|
+
text = buf.decode("utf-8", errors="ignore")
|
|
273
|
+
# Split; if we haven't reached the start the first fragment may be partial
|
|
274
|
+
parts = text.split("\n")
|
|
275
|
+
start = 1 if pos > 0 else 0
|
|
276
|
+
for line in reversed(parts[start:]):
|
|
277
|
+
if "token_count" not in line or not line.strip():
|
|
278
|
+
continue
|
|
279
|
+
try:
|
|
280
|
+
entry = json.loads(line)
|
|
281
|
+
if entry.get("type") == "event_msg":
|
|
282
|
+
info = entry.get("payload", {}).get("info", {})
|
|
283
|
+
usage = info.get("last_token_usage", {})
|
|
284
|
+
if usage:
|
|
285
|
+
total_in = usage.get("input_tokens", 0)
|
|
286
|
+
total_out = (usage.get("output_tokens", 0)
|
|
287
|
+
+ usage.get("reasoning_output_tokens", 0))
|
|
288
|
+
return total_in, total_out
|
|
289
|
+
except Exception:
|
|
290
|
+
continue
|
|
291
|
+
if pos == 0:
|
|
292
|
+
break
|
|
276
293
|
except Exception:
|
|
277
294
|
pass
|
|
278
295
|
return 0, 0
|
|
279
296
|
|
|
280
297
|
|
|
281
|
-
def
|
|
282
|
-
"""
|
|
298
|
+
def post_codex_main():
|
|
299
|
+
"""Delayed Codex token reader — spawned as background by post_usage_main.
|
|
300
|
+
|
|
301
|
+
Reads args from a pending JSON file, sleeps 2 s to let Codex flush the
|
|
302
|
+
token_count event, then scans the transcript and POSTs to the API.
|
|
303
|
+
"""
|
|
304
|
+
import time
|
|
305
|
+
if len(sys.argv) < 3:
|
|
306
|
+
sys.exit(0)
|
|
307
|
+
pending_path = Path(sys.argv[2])
|
|
283
308
|
try:
|
|
284
|
-
|
|
309
|
+
args = json.loads(pending_path.read_text())
|
|
310
|
+
pending_path.unlink(missing_ok=True)
|
|
285
311
|
except Exception:
|
|
286
312
|
sys.exit(0)
|
|
313
|
+
time.sleep(2)
|
|
314
|
+
transcript_path = args.get("transcript_path", "")
|
|
315
|
+
tokens_in, tokens_out = _scan_codex_tokens(transcript_path)
|
|
316
|
+
if tokens_in or tokens_out:
|
|
317
|
+
_post_usage(args.get("session_id"), args.get("tool_name"),
|
|
318
|
+
tokens_in, tokens_out, None)
|
|
319
|
+
sys.exit(0)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def post_usage_main():
|
|
323
|
+
"""PostToolUse hook entrypoint — exits immediately; heavy work is async."""
|
|
287
324
|
try:
|
|
288
|
-
|
|
325
|
+
data = json.load(sys.stdin)
|
|
289
326
|
except Exception:
|
|
290
|
-
|
|
327
|
+
sys.exit(0)
|
|
291
328
|
session_id = data.get("session_id")
|
|
292
329
|
tool_name = (data.get("tool_name") or "").lower()
|
|
293
330
|
tool_use_id = data.get("tool_use_id")
|
|
294
331
|
transcript_path = data.get("transcript_path")
|
|
295
|
-
|
|
332
|
+
is_codex = (tool_use_id or "").startswith("call_")
|
|
333
|
+
|
|
334
|
+
if is_codex and transcript_path:
|
|
335
|
+
# Write pending args; spawn delayed background reader so hook exits instantly
|
|
336
|
+
import uuid as _uuid
|
|
337
|
+
pending = GUARD_DIR / f"codex_pending_{_uuid.uuid4().hex[:8]}.json"
|
|
338
|
+
try:
|
|
339
|
+
pending.write_text(json.dumps({
|
|
340
|
+
"session_id": session_id,
|
|
341
|
+
"tool_name": tool_name,
|
|
342
|
+
"transcript_path": transcript_path,
|
|
343
|
+
}))
|
|
344
|
+
hook_path = Path(__file__).resolve()
|
|
345
|
+
subprocess.Popen(
|
|
346
|
+
[sys.executable, str(hook_path), "post-codex", str(pending)],
|
|
347
|
+
stdout=subprocess.DEVNULL,
|
|
348
|
+
stderr=subprocess.DEVNULL,
|
|
349
|
+
start_new_session=True,
|
|
350
|
+
)
|
|
351
|
+
except Exception:
|
|
352
|
+
pass
|
|
353
|
+
elif transcript_path:
|
|
296
354
|
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)
|
|
355
|
+
_post_usage(session_id, tool_name, tokens_input, tokens_output, None)
|
|
356
|
+
|
|
300
357
|
sys.exit(0)
|
|
301
358
|
|
|
302
359
|
|
|
@@ -336,6 +393,8 @@ def main():
|
|
|
336
393
|
if __name__ == "__main__":
|
|
337
394
|
if len(sys.argv) > 1 and sys.argv[1] == "post":
|
|
338
395
|
post_usage_main()
|
|
396
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "post-codex":
|
|
397
|
+
post_codex_main()
|
|
339
398
|
else:
|
|
340
399
|
main()
|
|
341
400
|
'''
|
|
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
|