codebase-explorer 2.2.0__py3-none-any.whl

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.
Files changed (47) hide show
  1. cbe/__init__.py +9 -0
  2. cbe/__main__.py +4 -0
  3. cbe/aggregation.py +183 -0
  4. cbe/api_completion.py +410 -0
  5. cbe/batching.py +283 -0
  6. cbe/catalog_query.py +376 -0
  7. cbe/checks.py +390 -0
  8. cbe/cli.py +434 -0
  9. cbe/concrete_facts.py +321 -0
  10. cbe/data/LICENSE.tiktoken +30 -0
  11. cbe/data/o200k_base.tiktoken +199998 -0
  12. cbe/declarations.py +537 -0
  13. cbe/docs.py +929 -0
  14. cbe/generate.py +316 -0
  15. cbe/grammars.py +741 -0
  16. cbe/graph.py +857 -0
  17. cbe/graph_backends/__init__.py +43 -0
  18. cbe/graph_backends/_calls.py +262 -0
  19. cbe/graph_backends/_paths.py +42 -0
  20. cbe/graph_backends/identity.py +93 -0
  21. cbe/graph_backends/imports.py +256 -0
  22. cbe/graph_backends/joern.py +1021 -0
  23. cbe/graph_backends/resolution.py +288 -0
  24. cbe/graph_backends/toolchain.py +396 -0
  25. cbe/grounding.py +57 -0
  26. cbe/host_run.py +1752 -0
  27. cbe/model_settings.py +80 -0
  28. cbe/projection.py +305 -0
  29. cbe/prompts.py +316 -0
  30. cbe/records.py +200 -0
  31. cbe/skeleton.py +474 -0
  32. cbe/source_context.py +709 -0
  33. cbe/symbols.py +136 -0
  34. cbe/token_budget.py +68 -0
  35. cbe/treename.py +398 -0
  36. cbe/usage.py +300 -0
  37. cbe_install/__init__.py +11 -0
  38. cbe_install/cli.py +434 -0
  39. cbe_install/resources/agents/cbe-writer.md +16 -0
  40. cbe_install/resources/codebase-explorer/SKILL.md +101 -0
  41. cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
  42. cbe_install/resources/codebase-explorer/references/codex.md +37 -0
  43. codebase_explorer-2.2.0.dist-info/METADATA +233 -0
  44. codebase_explorer-2.2.0.dist-info/RECORD +47 -0
  45. codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
  46. codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
  47. codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
cbe/usage.py ADDED
@@ -0,0 +1,300 @@
1
+ """Token usage and cost: exact harness records where they exist, labelled estimates otherwise."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+ import math
8
+ import os
9
+ import re
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from cbe.host_run import HostRunError, epoch, log_event, state_dir, locked_state, write_json
14
+ from cbe.token_budget import count_text_tokens
15
+
16
+
17
+ # USD per million tokens: input, output, cache write (5 minute), cache read.
18
+ # Anthropic first-party list prices as of 2026-09-25; pass --price to override.
19
+ PRICES = {
20
+ "claude-fable-5-1": (10.0, 50.0, 12.5, 0.25),
21
+ "claude-opus-5-5": (4.0, 20.0, 5.0, 0.20),
22
+ "claude-sonnet-5-5": (2.0, 10.0, 2.5, 0.20),
23
+ "claude-haiku-4-5": (1.0, 5.0, 1.25, 0.10),
24
+ }
25
+
26
+
27
+ # Rough per-subagent harness overhead (system prompt and tool definitions) for estimates.
28
+ DEFAULT_OVERHEAD_TOKENS = 20_000
29
+
30
+
31
+ def _claude_transcript_usage(path: Path) -> dict[str, Any]:
32
+ """Sum per-message usage; streamed messages repeat, so keep the max per id."""
33
+ per_message: dict[str, dict[str, int]] = {}
34
+ models: set[str] = set()
35
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
36
+ try:
37
+ record = json.loads(line)
38
+ except ValueError:
39
+ continue
40
+ message = record.get("message") if isinstance(record, dict) else None
41
+ if not isinstance(message, dict) or not isinstance(message.get("usage"), dict):
42
+ continue
43
+ key = message.get("id") or record.get("uuid") or str(len(per_message))
44
+ if message.get("model"):
45
+ models.add(message["model"])
46
+ usage = message["usage"]
47
+ seen = per_message.setdefault(key, {})
48
+ for field in ("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens"):
49
+ if isinstance(usage.get(field), int):
50
+ seen[field] = max(seen.get(field, 0), usage[field])
51
+ total = {field: sum(row.get(field, 0) for row in per_message.values())
52
+ for field in ("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens")}
53
+ return {"input_tokens": total["input_tokens"], "cache_write_tokens": total["cache_creation_input_tokens"],
54
+ "cache_read_tokens": total["cache_read_input_tokens"], "output_tokens": total["output_tokens"],
55
+ "turns": len(per_message), "model": ",".join(sorted(models))}
56
+
57
+
58
+ def _codex_rollout_usage(path: Path) -> tuple[str | None, dict[str, Any] | None]:
59
+ agent_path, model, usage = None, None, None
60
+ with path.open(encoding="utf-8", errors="replace") as stream:
61
+ for number, line in enumerate(stream):
62
+ try:
63
+ record = json.loads(line)
64
+ except ValueError:
65
+ continue
66
+ payload = record.get("payload") or {}
67
+ if record.get("type") == "session_meta":
68
+ spawn = ((payload.get("source") or {}).get("subagent") or {}) if isinstance(
69
+ payload.get("source"), dict) else {}
70
+ agent_path = (spawn.get("thread_spawn") or {}).get("agent_path")
71
+ if not agent_path:
72
+ return None, None
73
+ elif number == 0:
74
+ return None, None
75
+ if record.get("type") == "turn_context" and payload.get("model"):
76
+ model = payload["model"]
77
+ if payload.get("type") == "token_count" and isinstance(payload.get("info"), dict):
78
+ usage = payload["info"].get("total_token_usage") or usage
79
+ if not agent_path or not usage:
80
+ return agent_path, None
81
+ cached = int(usage.get("cached_input_tokens") or 0)
82
+ return agent_path, {"input_tokens": int(usage.get("input_tokens") or 0) - cached,
83
+ "cache_write_tokens": int(usage.get("cache_write_input_tokens") or 0),
84
+ "cache_read_tokens": cached, "output_tokens": int(usage.get("output_tokens") or 0),
85
+ "reasoning_output_tokens": int(usage.get("reasoning_output_tokens") or 0),
86
+ "model": model or ""}
87
+
88
+
89
+ def import_usage(run_dir: Path, harness: str, root: Path | None = None) -> dict[str, Any]:
90
+ """Read exact per-subagent usage that Claude Code or Codex already recorded."""
91
+ with locked_state(run_dir) as state:
92
+ since = epoch(state["created_at"]) - 60
93
+ tag = state["run_tag"]
94
+ found: dict[str, list[dict[str, Any]]] = {}
95
+ if harness == "claude-code":
96
+ base = root or Path.home() / ".claude" / "projects"
97
+ pattern = re.compile(rf"\bcbe {tag} (\S+)")
98
+ metas = base.rglob("agent-*.meta.json") if root else base.glob("*/*/subagents/agent-*.meta.json")
99
+ for meta in metas:
100
+ # Other sessions may delete their transcripts while we scan.
101
+ try:
102
+ if meta.stat().st_mtime < since:
103
+ continue
104
+ description = json.loads(meta.read_text(encoding="utf-8")).get("description") or ""
105
+ match = pattern.search(description)
106
+ transcript = meta.with_name(meta.name.replace(".meta.json", ".jsonl"))
107
+ if match and match.group(1) in state["tasks"] and transcript.is_file():
108
+ row = _claude_transcript_usage(transcript)
109
+ else:
110
+ continue
111
+ except (OSError, ValueError):
112
+ continue
113
+ row.update(agent=transcript.stem, source="claude-code subagent transcript")
114
+ found.setdefault(match.group(1), []).append(row)
115
+ elif harness == "codex":
116
+ base = root or Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")) / "sessions"
117
+ pattern = re.compile(rf"cbe_{tag}_(\S+)$")
118
+ for rollout in base.rglob("rollout-*.jsonl"):
119
+ try:
120
+ if rollout.stat().st_mtime < since:
121
+ continue
122
+ agent_path, row = _codex_rollout_usage(rollout)
123
+ except OSError:
124
+ continue
125
+ match = pattern.search((agent_path or "").rsplit("/", 1)[-1])
126
+ if match and row and match.group(1) in state["tasks"]:
127
+ row.update(agent=rollout.stem, source="codex subagent rollout")
128
+ found.setdefault(match.group(1), []).append(row)
129
+ else:
130
+ raise HostRunError("harness must be claude-code or codex")
131
+ for task_id, rows in found.items():
132
+ state["tasks"][task_id]["exact_usage"] = rows
133
+ log_event(run_dir, "usage", f"imported exact {harness} usage for {sorted(found)}",
134
+ harness=harness, tasks=sorted(found))
135
+ missing = [task_id for task_id, task in state["tasks"].items()
136
+ if task["attempts"] and not task.get("exact_usage")]
137
+ return {"harness": harness, "tasks_with_exact_usage": len(found),
138
+ "subagent_transcripts": sum(len(rows) for rows in found.values()),
139
+ "tasks_without_exact_usage": missing}
140
+
141
+
142
+ def _price_for(model: str, override: tuple[float, float, float, float] | None) -> tuple[tuple[float, ...] | None, str]:
143
+ if override is not None:
144
+ return override, "price given on the command line"
145
+ for name, price in PRICES.items():
146
+ if model == name or model.startswith(name + "-"):
147
+ return price, f"{name} list price (2026-09-25)"
148
+ return None, f"price unknown: {model or 'model not recorded'}"
149
+
150
+
151
+ def _dollars(row: dict[str, Any], price: tuple[float, ...] | None) -> float | None:
152
+ if price is None:
153
+ return None
154
+ return round((row.get("input_tokens", 0) * price[0] + row.get("output_tokens", 0) * price[1]
155
+ + row.get("cache_write_tokens", 0) * price[2] + row.get("cache_read_tokens", 0) * price[3])
156
+ / 1_000_000, 4)
157
+
158
+
159
+ def _sum_dollars(values: list[float | None]) -> float | None:
160
+ if any(value is None for value in values):
161
+ return None
162
+ return round(sum(value for value in values if value is not None), 4)
163
+
164
+
165
+ def _estimate(run_dir: Path, state: dict[str, Any], task: dict[str, Any], overhead: int) -> dict[str, Any]:
166
+ """A deliberately simple agent-loop model; label every number as an estimate.
167
+
168
+ A subagent reads its task, reads the listed files (usually in parallel),
169
+ writes the result, and replies. Each turn re-reads the cached context. When
170
+ the harness reported the subagent's final context size, that anchors the
171
+ model; otherwise the context is overhead + task + source + result.
172
+ """
173
+ prompt_path = state_dir(run_dir) / "tasks" / f"{task['id']}.md"
174
+ prompt = count_text_tokens(prompt_path.read_text(encoding="utf-8")) if prompt_path.is_file() else 0
175
+ source, files = 0, 0
176
+ if task["kind"] in {"module", "repair", "review"}:
177
+ paths = set(state["modules"][task["module"]]["files"])
178
+ source = sum(row["tokens"] for row in state["files"] if row["path"] in paths)
179
+ files = len(paths)
180
+ accepted = state_dir(run_dir) / "accepted" / f"{task['id']}.json"
181
+ output = count_text_tokens(accepted.read_text(encoding="utf-8")) if accepted.is_file() else 1_000
182
+ turns = 3 if not files else 4 + (files > 4)
183
+ reported = [entry["total_tokens"] for entry in task["usage"] if entry.get("total_tokens")]
184
+ final_context = (sum(reported) // len(reported)) if reported else overhead + prompt + source + output
185
+ start_context = max(final_context - source - output, prompt)
186
+ attempts = max(1, task["attempts"])
187
+ return {"input_tokens": 0, "cache_write_tokens": final_context * attempts,
188
+ "cache_read_tokens": (turns - 1) * (start_context + final_context) // 2 * attempts,
189
+ "output_tokens": output * 2 * attempts, "turns": turns * attempts,
190
+ "anchored": bool(reported)}
191
+
192
+
193
+ def cost(run_dir: Path, *, model: str | None = None, price: tuple[float, float, float, float] | None = None,
194
+ overhead: int = DEFAULT_OVERHEAD_TOKENS) -> dict[str, Any]:
195
+ if price is not None and (len(price) != 4 or any(not math.isfinite(value) or value < 0 for value in price)):
196
+ raise HostRunError("--price needs four finite, nonnegative rates: input,output,cache_write,cache_read")
197
+ with locked_state(run_dir) as state:
198
+ rows, totals = [], {"input_tokens": 0, "cache_write_tokens": 0, "cache_read_tokens": 0,
199
+ "output_tokens": 0}
200
+ bases: set[str] = set()
201
+ price_notes: set[str] = set()
202
+ for task in state["tasks"].values():
203
+ if not task["attempts"]:
204
+ continue
205
+ estimate = _estimate(run_dir, state, task, overhead)
206
+ if task.get("exact_usage"):
207
+ parts = task["exact_usage"]
208
+ basis = "exact"
209
+ else:
210
+ parts = [{**estimate, "model": model or ""}]
211
+ basis = "estimate"
212
+ effective_models = {part.get("model") or model or "" for part in parts}
213
+ estimate_model = next(iter(effective_models)) if len(effective_models) == 1 else ""
214
+ estimate_usd = _dollars(estimate, _price_for(estimate_model, price)[0])
215
+ bases.add(basis)
216
+ amounts = []
217
+ for part in parts:
218
+ chosen, note = _price_for(part.get("model") or model or "", price)
219
+ price_notes.add(note)
220
+ amounts.append(_dollars(part, chosen))
221
+ row = {"task": task["id"], "basis": basis, "subagents": len(parts) if basis == "exact" else task["attempts"],
222
+ **{field: sum(part.get(field, 0) for part in parts) for field in totals},
223
+ "usd_api_equivalent": _sum_dollars(amounts), "estimate_usd": estimate_usd,
224
+ "estimate_anchored_on_reported_context": estimate["anchored"],
225
+ "models": sorted({part.get("model") or "" for part in parts} - {""})}
226
+ reported = [entry for entry in task["usage"] if entry.get("total_tokens")]
227
+ if reported:
228
+ row["harness_reported_total_tokens"] = sum(entry["total_tokens"] for entry in reported)
229
+ rows.append(row)
230
+ for field in totals:
231
+ totals[field] += row[field]
232
+ totals["usd"] = _sum_dollars([row["usd_api_equivalent"] for row in rows])
233
+ basis = "exact" if bases == {"exact"} else "estimate" if bases == {"estimate"} else "mixed"
234
+ report = {
235
+ "basis": basis, "tasks": rows, "totals": totals,
236
+ "subagent_runs": sum(row["subagents"] for row in rows),
237
+ "estimate_usd": _sum_dollars([row["estimate_usd"] for row in rows]),
238
+ "price_basis": sorted(price_notes),
239
+ "statement": _statement(basis, totals["usd"] is not None),
240
+ }
241
+ write_json(state_dir(run_dir) / "cost.json", report)
242
+ (state_dir(run_dir) / "COST.md").write_text(_cost_markdown(report), encoding="utf-8")
243
+ return report
244
+
245
+
246
+ def _statement(basis: str, priced: bool) -> str:
247
+ tokens = {"exact": "Token counts are exact, read from the harness's own per-subagent usage records.",
248
+ "estimate": "Token counts are ESTIMATES from prompt and source sizes; the harness did not "
249
+ "provide exact per-subagent usage.",
250
+ "mixed": "Some token counts are exact (harness records) and the rest are ESTIMATES."}[basis]
251
+ pricing = ("Dollar figures apply the rates identified below." if priced else
252
+ "One or more model prices are unknown, so dollar totals are unknown. "
253
+ "Pass --price with current rates to calculate a price equivalent.")
254
+ return (tokens + " " + pricing + " This is not a bill: a subscription uses plan quota. "
255
+ "The token-use estimate excludes graph evidence files read by host authors. "
256
+ "The host agent's own orchestration turns are not included; quote the harness session total for those.")
257
+
258
+
259
+ def _money(value: float | None, decimals: int = 2) -> str:
260
+ return "unknown" if value is None else f"${value:.{decimals}f}"
261
+
262
+
263
+ def _cost_markdown(report: dict[str, Any]) -> str:
264
+ totals = report["totals"]
265
+ lines = ["# CBE run cost", "", report["statement"], "",
266
+ f"- Basis: **{report['basis']}**; subagent runs: {report['subagent_runs']}",
267
+ f"- Tokens: input {totals['input_tokens']:,}, cache write {totals['cache_write_tokens']:,}, "
268
+ f"cache read {totals['cache_read_tokens']:,}, output {totals['output_tokens']:,}",
269
+ f"- Price equivalent: {_money(totals['usd'])} ({'; '.join(report['price_basis'])})",
270
+ f"- Price of the token-use estimate: {_money(report['estimate_usd'])}", "",
271
+ "| Task | Basis | Subagents | Input | Cache write | Cache read | Output | USD equiv. |",
272
+ "|---|---|---:|---:|---:|---:|---:|---:|"]
273
+ for row in report["tasks"]:
274
+ lines.append(f"| {row['task']} | {row['basis']} | {row['subagents']} | {row['input_tokens']:,} | "
275
+ f"{row['cache_write_tokens']:,} | {row['cache_read_tokens']:,} | {row['output_tokens']:,} | "
276
+ f"{_money(row['usd_api_equivalent'], 4)} |")
277
+ return "\n".join(lines) + "\n"
278
+
279
+
280
+ def script_cost(run_dir: Path, *, price: list[float] | None = None) -> dict[str, Any]:
281
+ with (run_dir / "calls.csv").open(newline="", encoding="utf-8") as stream:
282
+ rows = list(csv.DictReader(stream))
283
+ fields = ("input_tokens", "output_tokens", "cache_tokens")
284
+ complete = {field: all(row[field] != "" for row in rows) for field in fields}
285
+ totals = {field: sum(int(row[field]) for row in rows if row[field]) for field in fields}
286
+ known = all(complete.values())
287
+ input_tokens, output_tokens, cache_tokens = (totals[field] for field in fields)
288
+ rates = price
289
+ return {"calls": len(rows),
290
+ **{field: totals[field] if complete[field] or field != "cache_tokens" else None for field in fields},
291
+ "token_totals_basis": "observed calls; missing usage is excluded, never estimated",
292
+ "calls_with_unknown_input": sum(row["input_tokens"] == "" for row in rows),
293
+ "calls_with_unknown_output": sum(row["output_tokens"] == "" for row in rows),
294
+ "observed_cache_tokens": cache_tokens,
295
+ "calls_with_unknown_cache": sum(row["cache_tokens"] == "" for row in rows),
296
+ "priced_usd": round((input_tokens * rates[0] + output_tokens * rates[1] + cache_tokens * rates[2]) / 1_000_000, 6)
297
+ if known and rates is not None else None,
298
+ "price_basis": ("configured USD per million input,output,cache tokens" if price is not None
299
+ else "unknown; configure API price_per_million: [input, output, cache]"),
300
+ "usage_complete": known, "seconds": round(sum(float(row["seconds"]) for row in rows), 3)}
@@ -0,0 +1,11 @@
1
+ """Install the CBE skill and its Claude Code subagent from the self-contained distribution."""
2
+
3
+ PRODUCT = "codebase-explorer"
4
+ BUSINESS_MODULE = "cbe.cli"
5
+ SKILLS = ("codebase-explorer",)
6
+ SOURCE_PARENT_LEVEL = 2
7
+ SOURCE_SKILL_BASE = "plugin/skills"
8
+ SOURCE_AGENT_BASE = "plugin/agents"
9
+ AGENTS_RESOURCE = "agents"
10
+ RUNTIME_SKILLS = ("codebase-explorer",)
11
+ RUNTIME_MODULES = ("cbe",)