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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.13
3
+ Version: 0.4.15
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.13"
7
+ version = "0.4.15"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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 _read_codex_tokens():
253
- """Read last_token_usage from the most recently modified Codex session file."""
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
- sessions_dir = Path.home() / ".codex" / "sessions"
256
- if not sessions_dir.exists():
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
- lines = _tail_lines(files[0])
262
- for line in reversed(lines):
263
- if "token_count" not in line:
264
- continue
265
- try:
266
- entry = json.loads(line)
267
- if entry.get("type") == "event_msg":
268
- info = entry.get("payload", {}).get("info", {})
269
- usage = info.get("last_token_usage", {})
270
- if usage:
271
- total_in = usage.get("input_tokens", 0)
272
- total_out = usage.get("output_tokens", 0) + usage.get("reasoning_output_tokens", 0)
273
- return total_in, total_out
274
- except Exception:
275
- continue
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 post_usage_main():
282
- """PostToolUse hook entrypointcaptures token usage and duration."""
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
- data = json.load(sys.stdin)
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
- (GUARD_DIR / "debug_post.json").write_text(json.dumps(data, indent=2))
325
+ data = json.load(sys.stdin)
289
326
  except Exception:
290
- pass
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
- if transcript_path:
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
- else:
298
- tokens_input, tokens_output = _read_codex_tokens()
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
  '''
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.13
3
+ Version: 0.4.15
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