oh-langfuse 0.1.43 → 0.1.44

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.
@@ -61,6 +61,7 @@ LOG_FILE = STATE_DIR / "codex_langfuse_notify.log"
61
61
 
62
62
  DEBUG = os.environ.get("CODEX_LANGFUSE_DEBUG", "").lower() == "true"
63
63
  MAX_CHARS = int(os.environ.get("CODEX_LANGFUSE_MAX_CHARS", "20000"))
64
+ MAX_SKILL_SCAN_CHARS = int(os.environ.get("CODEX_LANGFUSE_SKILL_SCAN_MAX_CHARS", "200000"))
64
65
  METRICS_SCHEMA_VERSION = "1.1"
65
66
  AGENT_NAME = "codex"
66
67
 
@@ -460,6 +461,7 @@ def build_interaction_metadata(
460
461
  def discover_known_skills(extra_roots: Optional[List[Path]] = None) -> set:
461
462
  roots = [
462
463
  CODEX_DIR / "skills",
464
+ CODEX_DIR / "plugins" / "cache",
463
465
  Path.home() / ".claude" / "skills",
464
466
  Path.home() / ".config" / "opencode" / "skill",
465
467
  ]
@@ -490,6 +492,71 @@ def _skill_id_segment(name: str) -> str:
490
492
  return (segment or "unknown")[:96]
491
493
 
492
494
 
495
+ def _skill_usage(name: str, detected_by: str, skill_call_id: str = "") -> Dict[str, str]:
496
+ clean = str(name or "").strip()
497
+ return {
498
+ "name": clean,
499
+ "skill_namespace": _skill_namespace(clean),
500
+ "detected_by": detected_by,
501
+ "skill_call_id": str(skill_call_id or "").strip(),
502
+ }
503
+
504
+
505
+ def _accept_skill_candidate(name: Any, known_skills: set, trusted: bool = False) -> str:
506
+ clean = str(name or "").strip()
507
+ if not clean:
508
+ return ""
509
+ if trusted or not known_skills or clean in known_skills:
510
+ return clean
511
+ return ""
512
+
513
+
514
+ def _collect_strings_limited(value: Any, out: List[str], remaining: List[int]) -> None:
515
+ if remaining[0] <= 0 or value is None:
516
+ return
517
+ if isinstance(value, str):
518
+ text = value[: remaining[0]]
519
+ if text:
520
+ out.append(text)
521
+ remaining[0] -= len(text)
522
+ return
523
+ if isinstance(value, (int, float, bool)):
524
+ return
525
+ if isinstance(value, list):
526
+ for item in value:
527
+ _collect_strings_limited(item, out, remaining)
528
+ if remaining[0] <= 0:
529
+ break
530
+ return
531
+ if isinstance(value, dict):
532
+ for item in value.values():
533
+ _collect_strings_limited(item, out, remaining)
534
+ if remaining[0] <= 0:
535
+ break
536
+
537
+
538
+ def _detect_skill_usages_from_text(text: str, known_skills: set) -> List[Dict[str, str]]:
539
+ found: List[Dict[str, str]] = []
540
+ if not text:
541
+ return found
542
+ seen: set = set()
543
+ for match in re.finditer(r"([A-Za-z]:)?[^\"'\n\r]*[\\/]+([^\\/\"'\n\r]+)[\\/]+SKILL\.md", text, re.IGNORECASE):
544
+ name = _accept_skill_candidate(match.group(2), known_skills)
545
+ if name and name not in seen:
546
+ seen.add(name)
547
+ found.append(_skill_usage(name, "skill_file_path"))
548
+ for match in re.finditer(r"Base directory for this skill:\s*([^\r\n]+)", text, re.IGNORECASE):
549
+ path_text = match.group(1)
550
+ path_match = re.search(r"[\\/](?:skills|skill)[\\/]([^\\/\"'\r\n]+)", path_text, re.IGNORECASE)
551
+ if not path_match:
552
+ continue
553
+ name = _accept_skill_candidate(path_match.group(1), known_skills)
554
+ if name and name not in seen:
555
+ seen.add(name)
556
+ found.append(_skill_usage(name, "skill_file_path"))
557
+ return found
558
+
559
+
493
560
  def detect_skill_usages(tool_calls: List[Dict[str, Any]], known_skills: set) -> List[Dict[str, str]]:
494
561
  found: List[Dict[str, str]] = []
495
562
  seen_call_ids: set = set()
@@ -507,19 +574,59 @@ def detect_skill_usages(tool_calls: List[Dict[str, Any]], known_skills: set) ->
507
574
  if dedupe_key in seen_call_ids:
508
575
  break
509
576
  seen_call_ids.add(dedupe_key)
510
- found.append({"name": name, "skill_namespace": _skill_namespace(name), "detected_by": "tool_call", "skill_call_id": call_id})
577
+ found.append(_skill_usage(name, "tool_call", call_id))
511
578
  break
512
579
  try:
513
580
  text = json.dumps(input_obj, ensure_ascii=False)
514
581
  except Exception:
515
582
  text = str(input_obj)
516
- for match in re.finditer(r"([A-Za-z]:)?[^\"'\n\r]*[\\/]+([^\\/\"'\n\r]+)[\\/]+SKILL\.md", text, re.IGNORECASE):
517
- candidate = match.group(2)
518
- if candidate and (candidate in known_skills or not known_skills):
519
- found.append({"name": candidate, "skill_namespace": _skill_namespace(candidate), "detected_by": "skill_file_path"})
583
+ found.extend(_detect_skill_usages_from_text(text, known_skills))
520
584
  return found
521
585
 
522
586
 
587
+ def _dedupe_turn_skill_usages(usages: List[Dict[str, str]]) -> List[Dict[str, str]]:
588
+ out: List[Dict[str, str]] = []
589
+ seen_call_ids: set = set()
590
+ seen_detected: set = set()
591
+ for usage in usages or []:
592
+ name = str(usage.get("name") or "").strip()
593
+ if not name:
594
+ continue
595
+ call_id = str(usage.get("skill_call_id") or "").strip()
596
+ if call_id:
597
+ key = f"call:{call_id}"
598
+ if key in seen_call_ids:
599
+ continue
600
+ seen_call_ids.add(key)
601
+ out.append(usage)
602
+ continue
603
+ detected_by = str(usage.get("detected_by") or "")
604
+ if detected_by == "skill_file_path":
605
+ key = f"{name}:{detected_by}"
606
+ if key in seen_detected:
607
+ continue
608
+ seen_detected.add(key)
609
+ out.append(usage)
610
+ return out
611
+
612
+
613
+ def detect_turn_skill_usages(material: Dict[str, Any], known_skills: set) -> List[Dict[str, str]]:
614
+ found = list(detect_skill_usages(material.get("tool_calls") or [], known_skills))
615
+ sources = [
616
+ material.get("user_text"),
617
+ material.get("assistant_text"),
618
+ material.get("skill_detection_sources"),
619
+ ]
620
+ strings: List[str] = []
621
+ remaining = [max(0, MAX_SKILL_SCAN_CHARS)]
622
+ for source in sources:
623
+ _collect_strings_limited(source, strings, remaining)
624
+ if remaining[0] <= 0:
625
+ break
626
+ found.extend(_detect_skill_usages_from_text("\n".join(strings), known_skills))
627
+ return _dedupe_turn_skill_usages(found)
628
+
629
+
523
630
  def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str]]) -> List[Dict[str, Any]]:
524
631
  events: List[Dict[str, Any]] = []
525
632
  deduped: List[Dict[str, str]] = []
@@ -615,14 +722,16 @@ def usage_details_from_codex(usage: Dict[str, Any]) -> Dict[str, int]:
615
722
 
616
723
 
617
724
  def collect_turn_material(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
618
- user_texts: List[str] = []
619
- assistant_texts: List[str] = []
620
- tool_calls: List[Dict[str, Any]] = []
621
- tool_results: List[Dict[str, Any]] = []
622
-
623
- for row in rows:
624
- row_type = row.get("type")
625
- payload = get_payload(row)
725
+ user_texts: List[str] = []
726
+ assistant_texts: List[str] = []
727
+ tool_calls: List[Dict[str, Any]] = []
728
+ tool_results: List[Dict[str, Any]] = []
729
+ skill_detection_sources: List[Any] = []
730
+
731
+ for row in rows:
732
+ row_type = row.get("type")
733
+ payload = get_payload(row)
734
+ skill_detection_sources.append(payload or row)
626
735
 
627
736
  if row_type == "response_item":
628
737
  item_type = payload.get("type")
@@ -664,10 +773,11 @@ def collect_turn_material(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
664
773
 
665
774
  return {
666
775
  "user_text": "\n\n".join(user_texts[-3:]),
667
- "assistant_text": "\n\n".join(assistant_texts),
668
- "tool_calls": tool_calls,
669
- "tool_results": tool_results,
670
- }
776
+ "assistant_text": "\n\n".join(assistant_texts),
777
+ "tool_calls": tool_calls,
778
+ "tool_results": tool_results,
779
+ "skill_detection_sources": skill_detection_sources,
780
+ }
671
781
 
672
782
 
673
783
  def emit_codex_turn(
@@ -686,7 +796,7 @@ def emit_codex_turn(
686
796
  model = first_string(meta.get("model"), meta.get("model_provider")) or "codex"
687
797
  tool_calls = material.get("tool_calls") or []
688
798
  tool_results = material.get("tool_results") or []
689
- skill_usages = detect_skill_usages(tool_calls, discover_known_skills())
799
+ skill_usages = detect_turn_skill_usages(material, discover_known_skills())
690
800
  interaction_id = build_interaction_id("codex", session_id, turn_num)
691
801
  skill_use_events = build_skill_use_events(interaction_id, skill_usages)
692
802
  interaction_meta = build_interaction_metadata(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",