Skip to content

Commit 5be4ab1

Browse files
committed
feat: add project attribution
1 parent 92beed3 commit 5be4ab1

15 files changed

Lines changed: 513 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ codex-usage-tracker dashboard --output /tmp/codex-usage-dashboard.html
5959
codex-usage-tracker serve-dashboard --help
6060
codex-usage-tracker init-allowance --output /tmp/codex-usage-allowance.json
6161
codex-usage-tracker init-thresholds --output /tmp/codex-usage-thresholds.json
62+
codex-usage-tracker init-projects --output /tmp/codex-usage-projects.json
6263
codex-usage-tracker support-bundle --output /tmp/codex-usage-support.json
6364
codex-usage-tracker pricing-coverage
6465
codex-usage-tracker summary --preset by-subagent-role

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Use this when you want to answer questions like:
3030
- Do long-running chats get more expensive over time?
3131
- Are subagents, auto-reviews, or review passes attached to the right parent work?
3232
- Which calls have low cache reuse, high context-window pressure, or large reasoning output?
33-
- Which active project directories are consuming the most usage?
33+
- Which projects, project tags, or active directories are consuming the most usage?
3434
- Did a change in workflow, model choice, or reasoning mode improve efficiency?
3535

3636
The dashboard is intentionally split into three views:
@@ -241,6 +241,8 @@ Show a summary:
241241

242242
```bash
243243
codex-usage-tracker summary --group-by model
244+
codex-usage-tracker summary --group-by project
245+
codex-usage-tracker summary --group-by project_tag
244246
codex-usage-tracker summary --group-by thread --limit 20
245247
codex-usage-tracker summary --preset today
246248
codex-usage-tracker summary --preset last-7-days
@@ -303,6 +305,14 @@ codex-usage-tracker init-thresholds
303305

304306
Edit `~/.codex-usage-tracker/thresholds.json` to adjust the aggregate-only thresholds used for low cache reuse, high context pressure, high uncached input, large cumulative threads, reasoning-output spikes, large low-output calls, and high estimated cost. The dashboard uses these values for presets, insight cards, row recommendations, and thread lifecycle summaries.
305307

308+
Enable optional project aliases, ignored paths, and tags:
309+
310+
```bash
311+
codex-usage-tracker init-projects
312+
```
313+
314+
Edit `~/.codex-usage-tracker/projects.json` to map stable project hashes, repo roots, or project names to friendlier aliases, ignored paths, and tags. The tracker derives project identity from `cwd` and local Git metadata when available: repo root, repo name, current branch, and a hashed remote origin. It does not store or display the full remote URL.
315+
306316
Credit usage estimates are calculated from Codex's aggregate input, cached-input, and output token counters using the bundled OpenAI Codex rate-card snapshot from `https://help.openai.com/en/articles/20001106-codex-rate-card` and `https://developers.openai.com/codex/pricing`. Direct model matches are marked exact. Local aliases and inferred labels, such as code-review usage mapped to GPT-5.3-Codex, are marked estimated. Normal reports do not contact the network for allowance or credit estimates.
307317

308318
### Usage And Allowance Accuracy

docs/dashboard-guide.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ codex-usage-tracker init-allowance
2222

2323
To tune review thresholds locally, run `codex-usage-tracker init-thresholds` and edit `~/.codex-usage-tracker/thresholds.json`. These thresholds control low-cache, high-context, high-uncached-input, large-thread, reasoning-spike, low-output, and high-cost recommendations.
2424

25+
To tune project attribution locally, run `codex-usage-tracker init-projects` and edit `~/.codex-usage-tracker/projects.json`. The dashboard derives project name, relative cwd, branch, tags, and a hashed remote origin from aggregate `cwd` and local Git metadata when available.
26+
2527
The server keeps the HTML aggregate-only and enables two live features:
2628

2729
- `Refresh` rescans local Codex logs and updates the dashboard rows.
@@ -59,6 +61,7 @@ Use `Calls` view when you want to inspect individual model calls.
5961
- The top cards include cached input, uncached input, Codex credit usage, and optional usage remaining instead of estimated-token, unpriced-token, and price-coverage counters.
6062
- A `Parser warnings` chip appears only when the latest refresh reports skipped token events, missing expected token fields, invalid counters, duplicate cumulative snapshots, or unknown event shapes. Use `codex-usage-tracker inspect-log <path>` to inspect a suspect log without writing to SQLite.
6163
- Search matches thread, cwd, model, session id, turn id, subagent role, and parent thread fields.
64+
- Search also matches derived project names, project-relative cwd values, tags, branch names, and redacted remote labels.
6265
- The cards summarize only the currently visible filtered rows.
6366
- Time values are shown in your browser's local date/time format while sorting still uses the logged timestamp.
6467
- Click a column header like `Time`, `Thread`, `Tokens`, `Cost`, or `Cache` to sort. Use the sort menu for `Highest Codex credits`. Click the same header again to reverse the direction.

src/codex_usage_tracker/cli.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
DEFAULT_MARKETPLACE_PATH,
2626
DEFAULT_PLUGIN_LINK,
2727
DEFAULT_PRICING_PATH,
28+
DEFAULT_PROJECTS_PATH,
2829
DEFAULT_SUPPORT_BUNDLE_PATH,
2930
DEFAULT_THRESHOLDS_PATH,
3031
)
3132
from codex_usage_tracker.parser import inspect_log, load_session_index
3233
from codex_usage_tracker.plugin_installer import install_plugin, uninstall_plugin
34+
from codex_usage_tracker.projects import write_project_template
3335
from codex_usage_tracker.pricing import (
3436
OPENAI_PRICING_MD_URL,
3537
VALID_PRICING_TIERS,
@@ -83,6 +85,7 @@ def _build_parser() -> argparse.ArgumentParser:
8385
parser.add_argument("--pricing", type=Path, default=DEFAULT_PRICING_PATH)
8486
parser.add_argument("--allowance", type=Path, default=DEFAULT_ALLOWANCE_PATH)
8587
parser.add_argument("--thresholds", type=Path, default=DEFAULT_THRESHOLDS_PATH)
88+
parser.add_argument("--projects", type=Path, default=DEFAULT_PROJECTS_PATH)
8689
subparsers = parser.add_subparsers(dest="command", required=True)
8790
_add_setup_parser(subparsers)
8891
_add_doctor_parser(subparsers)
@@ -103,6 +106,7 @@ def _build_parser() -> argparse.ArgumentParser:
103106
_add_pricing_parsers(subparsers)
104107
_add_allowance_parser(subparsers)
105108
_add_threshold_parser(subparsers)
109+
_add_project_parser(subparsers)
106110
_add_support_bundle_parser(subparsers)
107111
return parser
108112

@@ -403,6 +407,17 @@ def _add_threshold_parser(
403407
thresholds.add_argument("--force", action="store_true")
404408

405409

410+
def _add_project_parser(
411+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
412+
) -> None:
413+
projects = subparsers.add_parser(
414+
"init-projects",
415+
help="Write a local template for project aliases, ignored paths, and tags",
416+
)
417+
projects.add_argument("--output", type=Path, default=None)
418+
projects.add_argument("--force", action="store_true")
419+
420+
406421
def _add_support_bundle_parser(
407422
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
408423
) -> None:
@@ -609,6 +624,7 @@ def _run_summary(args: argparse.Namespace) -> int:
609624
preset=args.preset,
610625
since=args.since,
611626
limit=args.limit,
627+
projects_path=args.projects,
612628
)
613629
print(report.render())
614630
return 0
@@ -640,6 +656,7 @@ def _run_dashboard(args: argparse.Namespace) -> int:
640656
allowance_path=args.allowance,
641657
since=args.since,
642658
thresholds_path=args.thresholds,
659+
projects_path=args.projects,
643660
)
644661
print(f"Wrote dashboard to {output}")
645662
if args.open:
@@ -658,6 +675,7 @@ def _run_open_dashboard(args: argparse.Namespace) -> int:
658675
allowance_path=args.allowance,
659676
since=args.since,
660677
thresholds_path=args.thresholds,
678+
projects_path=args.projects,
661679
)
662680
print(f"Opening dashboard at {output}")
663681
webbrowser.open(output.resolve().as_uri())
@@ -686,6 +704,7 @@ def _run_serve_dashboard(args: argparse.Namespace) -> int:
686704
include_archived=args.include_archived,
687705
context_api="disabled" if args.no_context_api else args.context_api,
688706
thresholds_path=args.thresholds,
707+
projects_path=args.projects,
689708
)
690709
return 0
691710

@@ -758,6 +777,12 @@ def _run_init_thresholds(args: argparse.Namespace) -> int:
758777
return 0
759778

760779

780+
def _run_init_projects(args: argparse.Namespace) -> int:
781+
output = write_project_template(args.output or args.projects, force=args.force)
782+
print(f"Wrote project attribution template to {output}")
783+
return 0
784+
785+
761786
def _run_support_bundle(args: argparse.Namespace) -> int:
762787
output = build_support_bundle(
763788
output_path=args.output,
@@ -766,6 +791,7 @@ def _run_support_bundle(args: argparse.Namespace) -> int:
766791
pricing_path=args.pricing,
767792
allowance_path=args.allowance,
768793
thresholds_path=args.thresholds,
794+
projects_path=args.projects,
769795
)
770796
print(f"Wrote privacy-preserving support bundle to {output}")
771797
print("Bundle excludes raw logs, prompts, assistant messages, tool output, and context text.")
@@ -795,6 +821,7 @@ def _run_support_bundle(args: argparse.Namespace) -> int:
795821
"update-pricing": _run_update_pricing,
796822
"init-allowance": _run_init_allowance,
797823
"init-thresholds": _run_init_thresholds,
824+
"init-projects": _run_init_projects,
798825
"support-bundle": _run_support_bundle,
799826
}
800827

src/codex_usage_tracker/dashboard.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
DEFAULT_ALLOWANCE_PATH,
2121
DEFAULT_DASHBOARD_PATH,
2222
DEFAULT_PRICING_PATH,
23+
DEFAULT_PROJECTS_PATH,
2324
DEFAULT_THRESHOLDS_PATH,
2425
)
2526
from codex_usage_tracker.pricing import annotate_rows_with_efficiency, load_pricing_config
27+
from codex_usage_tracker.projects import annotate_rows_with_project_identity, load_project_config
2628
from codex_usage_tracker.recommendations import (
2729
annotate_rows_with_recommendations,
2830
load_threshold_config,
@@ -44,6 +46,7 @@ def dashboard_payload(
4446
api_token: str | None = None,
4547
context_api_enabled: bool = False,
4648
thresholds_path: Path = DEFAULT_THRESHOLDS_PATH,
49+
projects_path: Path = DEFAULT_PROJECTS_PATH,
4750
) -> dict[str, object]:
4851
"""Return aggregate-only dashboard data without rendering HTML."""
4952

@@ -53,11 +56,13 @@ def dashboard_payload(
5356
pricing = load_pricing_config(pricing_path)
5457
allowance = load_allowance_config(allowance_path)
5558
thresholds = load_threshold_config(thresholds_path)
59+
projects = load_project_config(projects_path)
5660
annotated_rows = annotate_rows_with_allowance(
5761
annotate_rows_with_efficiency(rows, pricing),
5862
allowance,
5963
)
6064
annotated_rows = annotate_rows_with_recommendations(annotated_rows, thresholds)
65+
annotated_rows = annotate_rows_with_project_identity(annotated_rows, projects)
6166
allowance_summary = summarize_allowance_usage(annotated_rows, allowance)
6267
normalized_limit = _normalize_limit(limit)
6368
metadata = refresh_metadata(db_path)
@@ -85,6 +90,8 @@ def dashboard_payload(
8590
"action_thresholds": thresholds.thresholds,
8691
"thresholds_configured": thresholds.loaded and not thresholds.error,
8792
"thresholds_error": thresholds.error,
93+
"project_configured": projects.loaded and not projects.error,
94+
"project_config_error": projects.error,
8895
}
8996

9097

@@ -98,6 +105,7 @@ def generate_dashboard(
98105
api_token: str | None = None,
99106
context_api_enabled: bool = False,
100107
thresholds_path: Path = DEFAULT_THRESHOLDS_PATH,
108+
projects_path: Path = DEFAULT_PROJECTS_PATH,
101109
) -> Path:
102110
output_path.parent.mkdir(parents=True, exist_ok=True)
103111
guide_href = _dashboard_guide_href(output_path)
@@ -114,6 +122,7 @@ def generate_dashboard(
114122
api_token=api_token,
115123
context_api_enabled=context_api_enabled,
116124
thresholds_path=thresholds_path,
125+
projects_path=projects_path,
117126
),
118127
ensure_ascii=True,
119128
).replace("</", "<\\/")

src/codex_usage_tracker/paths.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
DEFAULT_PRICING_PATH = APP_DIR / "pricing.json"
1313
DEFAULT_ALLOWANCE_PATH = APP_DIR / "allowance.json"
1414
DEFAULT_THRESHOLDS_PATH = APP_DIR / "thresholds.json"
15+
DEFAULT_PROJECTS_PATH = APP_DIR / "projects.json"
1516
DEFAULT_CODEX_HOME = Path.home() / ".codex"
1617
DEFAULT_PLUGIN_LINK = Path.home() / "plugins" / "codex-usage-tracker"
1718
DEFAULT_MARKETPLACE_PATH = Path.home() / ".agents" / "plugins" / "marketplace.json"

src/codex_usage_tracker/plugin_data/dashboard/dashboard.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,11 @@ const initialPayload = JSON.parse(document.getElementById('usage-data').textCont
427427
const haystack = [
428428
rowThreadLabel(row),
429429
row.cwd,
430+
row.project_name,
431+
row.project_relative_cwd,
432+
Array.isArray(row.project_tags) ? row.project_tags.join(' ') : '',
433+
row.git_branch,
434+
row.git_remote_label,
430435
row.model,
431436
row.effort,
432437
row.session_id,
@@ -1341,6 +1346,8 @@ const initialPayload = JSON.parse(document.getElementById('usage-data').textCont
13411346
<h3>Thread narrative</h3>
13421347
${fieldsList([
13431348
['Thread', attachment.label],
1349+
['Project', row.project_name || 'Unknown project'],
1350+
['Project tags', Array.isArray(row.project_tags) && row.project_tags.length ? row.project_tags.join(', ') : 'None'],
13441351
['Thread attachment', attachment.relation],
13451352
['Source', sourceLabel(row)],
13461353
['Parent thread', resolvedParentThreadName(row) || 'None'],
@@ -1373,6 +1380,10 @@ const initialPayload = JSON.parse(document.getElementById('usage-data').textCont
13731380
['Parent session', row.parent_session_id || 'None'],
13741381
['Parent updated', resolvedParentSessionUpdatedAt(row) ? formatTimestamp(resolvedParentSessionUpdatedAt(row)) : 'None'],
13751382
['Cwd', row.cwd],
1383+
['Project cwd', row.project_relative_cwd || '.'],
1384+
['Git branch', row.git_branch || 'Unknown'],
1385+
['Remote label', row.git_remote_label || 'None'],
1386+
['Remote hash', row.git_remote_hash || 'None'],
13761387
])}
13771388
${detailCollapse('Source file and line', [
13781389
['Source line', `${row.source_file}:${row.line_number}`],

src/codex_usage_tracker/plugin_data/docs/dashboard-guide.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ <h2>Open The Dashboard</h2>
8383
codex-usage-tracker serve-dashboard --open</code></pre>
8484
<p>For optional allowance context, run <code>codex-usage-tracker init-allowance</code> and copy current 5-hour or weekly remaining usage from Codex Usage or <code>/status</code> into the local template.</p>
8585
<p>To tune review thresholds locally, run <code>codex-usage-tracker init-thresholds</code> and edit <code>~/.codex-usage-tracker/thresholds.json</code>. These thresholds control low-cache, high-context, high-uncached-input, large-thread, reasoning-spike, low-output, and high-cost recommendations.</p>
86+
<p>To tune project attribution locally, run <code>codex-usage-tracker init-projects</code> and edit <code>~/.codex-usage-tracker/projects.json</code>. The dashboard derives project name, relative cwd, branch, tags, and a hashed remote origin from aggregate <code>cwd</code> and local Git metadata when available.</p>
8687
<p>The server enables live aggregate refresh and on-demand context loading. Static file mode can still filter, sort, and inspect aggregate fields, but cannot refresh logs or load context.</p>
8788
<p>The localhost server uses a random per-server token for refresh and context API calls, validates loopback <code>Host</code> and <code>Origin</code> headers, and can run as aggregate-only with <code>codex-usage-tracker serve-dashboard --no-context-api</code>.</p>
8889

@@ -104,6 +105,7 @@ <h2>Calls View</h2>
104105
<li><code>Cached input</code> and <code>Uncached input</code> make cache behavior visible without storing transcript text.</li>
105106
<li>A cost with <code>*</code> means the pricing row is a marked best-guess estimate.</li>
106107
<li>Codex credits are estimated from aggregate input, cached-input, and output counters using bundled or locally configured rate-card values.</li>
108+
<li>Search matches derived project names, project-relative cwd values, tags, branch names, and redacted remote labels.</li>
107109
<li><code>Usage Remaining</code> is not read from the logged-in account plan. Configure <code>~/.codex-usage-tracker/allowance.json</code> with values copied from Codex Settings &gt; Usage, the Codex Usage dashboard, or <code>/status</code> when you want current remaining allowance context.</li>
108110
<li>Call details include a recommended action and a "why flagged" explanation derived only from aggregate counters and pricing/allowance metadata.</li>
109111
<li>Raw aggregate identifiers and source file metadata are collapsed until you need them.</li>

0 commit comments

Comments
 (0)