conduct-cli 0.4.14__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.14 → conduct_cli-0.4.15}/PKG-INFO +1 -1
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/pyproject.toml +1 -1
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli/guard.py +86 -27
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/README.md +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/setup.cfg +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/setup.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.14 → conduct_cli-0.4.15}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.14 → 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
259
|
path = Path(transcript_path)
|
|
256
260
|
if not path.exists():
|
|
257
261
|
return 0, 0
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
273
293
|
except Exception:
|
|
274
294
|
pass
|
|
275
295
|
return 0, 0
|
|
276
296
|
|
|
277
297
|
|
|
278
|
-
def
|
|
279
|
-
"""
|
|
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])
|
|
280
308
|
try:
|
|
281
|
-
|
|
309
|
+
args = json.loads(pending_path.read_text())
|
|
310
|
+
pending_path.unlink(missing_ok=True)
|
|
282
311
|
except Exception:
|
|
283
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."""
|
|
284
324
|
try:
|
|
285
|
-
|
|
325
|
+
data = json.load(sys.stdin)
|
|
286
326
|
except Exception:
|
|
287
|
-
|
|
327
|
+
sys.exit(0)
|
|
288
328
|
session_id = data.get("session_id")
|
|
289
329
|
tool_name = (data.get("tool_name") or "").lower()
|
|
290
330
|
tool_use_id = data.get("tool_use_id")
|
|
291
331
|
transcript_path = data.get("transcript_path")
|
|
292
332
|
is_codex = (tool_use_id or "").startswith("call_")
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
295
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
|