Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff35050
Re-express UsageRow in OTel GenAI semantic conventions
duncankmckinnon Aug 6, 2026
3c0cb33
Drop Gemini and Cursor tracing platforms
duncankmckinnon Aug 6, 2026
2c09d32
Merge wb/drop-platforms into otel-genai-usage
duncankmckinnon Aug 6, 2026
efd95aa
Replace fabricated Claude usage fixture with scrubbed real-data subset
duncankmckinnon Aug 6, 2026
e7486f7
Add canonical usage read path with call-level dedup
duncankmckinnon Aug 6, 2026
0c59918
Wire six missing Claude hook events (10 -> 16)
duncankmckinnon Aug 6, 2026
d242541
Rewrite Claude usage extractor for OTel GenAI conventions
duncankmckinnon Aug 6, 2026
2f3a56a
Add edge-case coverage for Claude usage extractor
duncankmckinnon Aug 6, 2026
aee51fd
Merge wb/usage-reader into otel-genai-usage
duncankmckinnon Aug 6, 2026
e5373fe
Merge wb/claude-hooks into otel-genai-usage
duncankmckinnon Aug 6, 2026
e0548d8
Move daily aggregation onto deduplicated read path
duncankmckinnon Aug 6, 2026
0ec51ef
Rebuild usage index on OTel GenAI schema, key by call_id
duncankmckinnon Aug 6, 2026
be95785
Update usage web views for OTel GenAI schema
duncankmckinnon Aug 6, 2026
3a89e37
Update usage CLI for OTel GenAI columns; add usage reset
duncankmckinnon Aug 6, 2026
301fa5b
Add gap-filling tests for usage CLI reset and per-session filters
duncankmckinnon Aug 6, 2026
41ab03c
Address usage CLI review: restore reindex/errors tests, harden reset …
duncankmckinnon Aug 6, 2026
6400197
Merge wb/usage-aggregate into otel-genai-usage
duncankmckinnon Aug 6, 2026
125e6c4
Merge wb/usage-cli into otel-genai-usage
duncankmckinnon Aug 6, 2026
705f4a9
Merge wb/usage-web into otel-genai-usage
duncankmckinnon Aug 6, 2026
781536a
Fix UsageRow rewrite fallout: Codex extractor + index test suite
duncankmckinnon Aug 6, 2026
9b59c46
reformatting
duncankmckinnon Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,13 @@ thirdeye-claude-stop-failure = "thirdeye.platforms.claude.hooks:stop_failure"
thirdeye-claude-notification = "thirdeye.platforms.claude.hooks:notification"
thirdeye-claude-permission-request = "thirdeye.platforms.claude.hooks:permission_request"
thirdeye-claude-session-end = "thirdeye.platforms.claude.hooks:session_end"
thirdeye-claude-post-tool-use-failure = "thirdeye.platforms.claude.hooks:post_tool_use_failure"
thirdeye-claude-subagent-start = "thirdeye.platforms.claude.hooks:subagent_start"
thirdeye-claude-user-prompt-expansion = "thirdeye.platforms.claude.hooks:user_prompt_expansion"
thirdeye-claude-pre-compact = "thirdeye.platforms.claude.hooks:pre_compact"
thirdeye-claude-post-compact = "thirdeye.platforms.claude.hooks:post_compact"
thirdeye-claude-permission-denied = "thirdeye.platforms.claude.hooks:permission_denied"
thirdeye-codex-notify = "thirdeye.platforms.codex.hooks:notify"
thirdeye-gemini-session-start = "thirdeye.platforms.gemini.hooks:session_start"
thirdeye-gemini-session-end = "thirdeye.platforms.gemini.hooks:session_end"
thirdeye-gemini-before-agent = "thirdeye.platforms.gemini.hooks:before_agent"
thirdeye-gemini-after-agent = "thirdeye.platforms.gemini.hooks:after_agent"
thirdeye-gemini-before-model = "thirdeye.platforms.gemini.hooks:before_model"
thirdeye-gemini-after-model = "thirdeye.platforms.gemini.hooks:after_model"
thirdeye-gemini-before-tool = "thirdeye.platforms.gemini.hooks:before_tool"
thirdeye-gemini-after-tool = "thirdeye.platforms.gemini.hooks:after_tool"
thirdeye-cursor-hook = "thirdeye.platforms.cursor.hook:main"

[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8", "wheel"]
Expand Down
78 changes: 70 additions & 8 deletions src/thirdeye/commands/add.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,100 @@
from __future__ import annotations

import json
from collections.abc import Iterable
from pathlib import Path

import click

from thirdeye.platforms.base import Platform
from thirdeye.platforms.claude.install import ClaudePlatform
from thirdeye.platforms.codex.install import CodexPlatform
from thirdeye.platforms.cursor.install import CursorPlatform
from thirdeye.platforms.gemini.install import GeminiPlatform

PLATFORMS: dict[str, type[Platform]] = {
"claude": ClaudePlatform,
"gemini": GeminiPlatform,
"codex": CodexPlatform,
"cursor": CursorPlatform,
}

# Config files that may still reference console scripts for platforms this
# version no longer installs (Gemini and Cursor). Deleting those platforms
# leaves their hook entries orphaned in other tools' config, firing a missing
# binary on every event, so `thirdeye add --list` warns about them.
ORPHAN_CONFIG_PATHS: tuple[Path, ...] = (
Path.home() / ".gemini" / "settings.json",
Path.home() / ".cursor" / "hooks.json",
)


def _platform_options(fn):
fn = click.option("--cursor", "platform_flag", flag_value="cursor", help="Cursor.")(fn)
fn = click.option("--codex", "platform_flag", flag_value="codex", help="Codex CLI.")(fn)
fn = click.option("--gemini", "platform_flag", flag_value="gemini", help="Gemini CLI.")(fn)
fn = click.option("--claude", "platform_flag", flag_value="claude", help="Claude Code.")(fn)
return fn


def _resolve_platform(platform_flag: str | None) -> Platform:
if not platform_flag:
raise click.UsageError("Pick a platform: --claude, --gemini, --codex, --cursor")
raise click.UsageError("Pick a platform: --claude, --codex")
return PLATFORMS[platform_flag]()


def _is_stale_command(command: str) -> bool:
"""True if a hook command targets a removed-platform console script."""
name = Path(command).name
return name.startswith("thirdeye-gemini-") or name == "thirdeye-cursor-hook"


def find_orphaned_hooks(
config_paths: Iterable[Path] = ORPHAN_CONFIG_PATHS,
) -> list[tuple[Path, str]]:
"""Return (config_file, stale_command) for each removed-platform hook found.

A command is stale when its basename starts with "thirdeye-gemini-" or
equals "thirdeye-cursor-hook". Missing or malformed files yield nothing.
"""
found: list[tuple[Path, str]] = []
for path in config_paths:
try:
data = json.loads(path.read_text())
except (OSError, json.JSONDecodeError):
continue
for command in _iter_commands(data):
if _is_stale_command(command):
found.append((path, command))
return found


def _iter_commands(node: object) -> Iterable[str]:
"""Yield every string under a "command" key anywhere in a nested structure."""
if isinstance(node, dict):
for key, value in node.items():
if key == "command" and isinstance(value, str):
yield value
else:
yield from _iter_commands(value)
elif isinstance(node, list):
for item in node:
yield from _iter_commands(item)


@click.command(help="Install tracing hooks for an agentic platform.")
@click.option(
"--list",
"list_platforms",
is_flag=True,
help="List supported platforms and warn about orphaned hooks from removed platforms.",
)
@_platform_options
def add(platform_flag: str | None) -> None:
def add(platform_flag: str | None, list_platforms: bool) -> None:
if list_platforms:
click.echo("Supported platforms:")
for name in PLATFORMS:
click.echo(f" {name}")
for path, command in find_orphaned_hooks(ORPHAN_CONFIG_PATHS):
click.echo(
f"Warning: {path} still references removed hook {command!r}. "
"Remove it from that tool's config to stop the missing-binary error.",
)
return
platform = _resolve_platform(platform_flag)
platform.install()
click.echo(f"Installed tracing for {platform.display_name}")
Expand Down
5 changes: 2 additions & 3 deletions src/thirdeye/commands/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ def run_cmd(session_prefix, using, agent, background, as_json, save):

if agent not in list_agent_names(config.root):
raise click.ClickException(
f"unknown agent {agent!r} — choose one of "
f"{', '.join(list_agent_names(config.root))}"
f"unknown agent {agent!r} — choose one of {', '.join(list_agent_names(config.root))}"
)

if background:
Expand Down Expand Up @@ -267,7 +266,7 @@ def status_cmd(session_prefix, as_json):
click.echo("No background eval jobs.")
return
click.echo(
f"{'JOB':<28} {'SESSION':<14} {'USING':<18} {'AGENT':<8} " f"{'STATUS':<10} {'STARTED':<26}"
f"{'JOB':<28} {'SESSION':<14} {'USING':<18} {'AGENT':<8} {'STATUS':<10} {'STARTED':<26}"
)
for j in jobs:
click.echo(
Expand Down
2 changes: 1 addition & 1 deletion src/thirdeye/commands/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


@click.command(help="Read newline-delimited JSON events from stdin and append them to a session.")
@click.option("--platform", required=True, help="Platform name (e.g. claude, cursor).")
@click.option("--platform", required=True, help="Platform name (e.g. claude, codex).")
@click.option("--session-id", default=None, help="Session ID. Generated if omitted.")
@click.option("--cwd", default=None, help="Working directory for the session.")
def ingest(platform: str, session_id: str | None, cwd: str | None) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/thirdeye/commands/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def ui(host: str, port: int, no_browser: bool) -> None:
msg = str(e)
if "starlette" in msg or "uvicorn" in msg or "jinja2" in msg:
raise click.ClickException(
"The UI requires the 'ui' extra. Install with:\n" " pip install 'thrdi[ui]'"
"The UI requires the 'ui' extra. Install with:\n pip install 'thrdi[ui]'"
) from e
raise

Expand Down
Loading
Loading