Skip to content

Commit ef19f9a

Browse files
committed
feat: add setup lifecycle commands
1 parent 710c105 commit ef19f9a

11 files changed

Lines changed: 618 additions & 13 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ python scripts/check_release.py --dist
5454
git diff --check
5555
codex-usage-tracker update-pricing --output /tmp/codex-usage-pricing.json
5656
codex-usage-tracker doctor
57+
codex-usage-tracker doctor --suggest-repair
5758
codex-usage-tracker dashboard --output /tmp/codex-usage-dashboard.html
5859
codex-usage-tracker serve-dashboard --help
5960
codex-usage-tracker init-allowance --output /tmp/codex-usage-allowance.json
61+
codex-usage-tracker support-bundle --output /tmp/codex-usage-support.json
6062
codex-usage-tracker pricing-coverage
6163
codex-usage-tracker summary --preset by-subagent-role
6264
codex-usage-tracker expensive --limit 5

README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Open a Codex session on your machine and paste this:
8080
```text
8181
Install and configure Codex Usage Tracker from https://github.com/douglasmonsky/codex-usage-tracker.
8282
Use pipx if it is available. If pipx is missing, install it with Homebrew or use a local virtual environment.
83-
After installation, run codex-usage-tracker install-plugin, update-pricing, refresh, doctor, and serve-dashboard --open.
83+
After installation, run codex-usage-tracker setup and serve-dashboard --open.
8484
Verify the dashboard opens locally and tell me the dashboard URL plus whether I need to restart Codex for plugin discovery.
8585
```
8686

@@ -90,10 +90,7 @@ Codex should run roughly:
9090
brew install pipx
9191
pipx ensurepath
9292
pipx install "git+https://github.com/douglasmonsky/codex-usage-tracker.git"
93-
codex-usage-tracker install-plugin
94-
codex-usage-tracker update-pricing
95-
codex-usage-tracker refresh
96-
codex-usage-tracker doctor
93+
codex-usage-tracker setup
9794
codex-usage-tracker serve-dashboard --open
9895
```
9996

@@ -107,18 +104,19 @@ Run:
107104
brew install pipx
108105
pipx ensurepath
109106
pipx install "git+https://github.com/douglasmonsky/codex-usage-tracker.git"
110-
codex-usage-tracker install-plugin
111-
codex-usage-tracker update-pricing
112-
codex-usage-tracker refresh
107+
codex-usage-tracker setup
113108
codex-usage-tracker serve-dashboard --open
114109
```
115110

116-
`install-plugin` creates `~/plugins/codex-usage-tracker`, writes a package-owned `.mcp.json` that points at the installed Python executable, and updates `~/.agents/plugins/marketplace.json`. Restart Codex after registration so it discovers the plugin.
111+
`setup` installs or refreshes the package-owned plugin wrapper, initializes a local pricing template when pricing is missing, refreshes the aggregate index, runs `doctor`, prints a success/failure summary, and tells you whether Codex needs a restart for plugin discovery.
112+
113+
`install-plugin` is still available when you only want plugin registration. It creates `~/plugins/codex-usage-tracker`, writes a package-owned `.mcp.json` that points at the installed Python executable, and updates `~/.agents/plugins/marketplace.json`. Restart Codex after registration so it discovers the plugin.
117114

118115
## Fastest Useful Workflow
119116

120117
```bash
121118
codex-usage-tracker update-pricing
119+
codex-usage-tracker setup
122120
codex-usage-tracker serve-dashboard --open
123121
```
124122

@@ -165,6 +163,18 @@ codex-usage-tracker --version
165163
python -m codex_usage_tracker --version
166164
```
167165

166+
Run or refresh local lifecycle tasks:
167+
168+
```bash
169+
codex-usage-tracker setup
170+
codex-usage-tracker upgrade-plugin
171+
codex-usage-tracker uninstall-plugin
172+
codex-usage-tracker reset-db --yes
173+
codex-usage-tracker support-bundle --output ~/.codex-usage-tracker/support-bundle.json
174+
```
175+
176+
`support-bundle` writes package, Python, OS, doctor, database schema, parser diagnostics, pricing status, and allowance status. It does not include raw logs, prompts, assistant messages, tool output, or context text.
177+
168178
Inspect a single Codex log without writing to SQLite:
169179

170180
```bash

docs/dashboard-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This guide uses synthetic aggregate data. The screenshots do not contain prompts
99
For the best experience, run the localhost dashboard server:
1010

1111
```bash
12+
codex-usage-tracker setup
1213
codex-usage-tracker update-pricing
1314
codex-usage-tracker serve-dashboard --open
1415
```

src/codex_usage_tracker/cli.py

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
DEFAULT_MARKETPLACE_PATH,
2626
DEFAULT_PLUGIN_LINK,
2727
DEFAULT_PRICING_PATH,
28+
DEFAULT_SUPPORT_BUNDLE_PATH,
2829
)
2930
from codex_usage_tracker.parser import inspect_log, load_session_index
30-
from codex_usage_tracker.plugin_installer import install_plugin
31+
from codex_usage_tracker.plugin_installer import install_plugin, uninstall_plugin
3132
from codex_usage_tracker.pricing import (
3233
OPENAI_PRICING_MD_URL,
3334
VALID_PRICING_TIERS,
@@ -47,7 +48,9 @@
4748
export_usage_csv,
4849
query_session_usage,
4950
refresh_usage_index,
51+
reset_usage_database,
5052
)
53+
from codex_usage_tracker.support import build_support_bundle
5154
from codex_usage_tracker.server import serve_dashboard
5255

5356

@@ -78,11 +81,15 @@ def _build_parser() -> argparse.ArgumentParser:
7881
parser.add_argument("--pricing", type=Path, default=DEFAULT_PRICING_PATH)
7982
parser.add_argument("--allowance", type=Path, default=DEFAULT_ALLOWANCE_PATH)
8083
subparsers = parser.add_subparsers(dest="command", required=True)
84+
_add_setup_parser(subparsers)
8185
_add_doctor_parser(subparsers)
8286
_add_install_plugin_parser(subparsers)
87+
_add_upgrade_plugin_parser(subparsers)
88+
_add_uninstall_plugin_parser(subparsers)
8389
_add_refresh_parser(subparsers)
8490
_add_inspect_log_parser(subparsers)
8591
_add_rebuild_index_parser(subparsers)
92+
_add_reset_db_parser(subparsers)
8693
_add_summary_parser(subparsers)
8794
_add_session_parser(subparsers)
8895
_add_context_parser(subparsers)
@@ -92,9 +99,39 @@ def _build_parser() -> argparse.ArgumentParser:
9299
_add_export_parser(subparsers)
93100
_add_pricing_parsers(subparsers)
94101
_add_allowance_parser(subparsers)
102+
_add_support_bundle_parser(subparsers)
95103
return parser
96104

97105

106+
def _add_setup_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
107+
setup = subparsers.add_parser(
108+
"setup",
109+
help="Run first-time setup: plugin install, pricing init, refresh, and doctor",
110+
)
111+
setup.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
112+
setup.add_argument("--include-archived", action="store_true")
113+
setup.add_argument("--plugin-dir", type=Path, default=DEFAULT_PLUGIN_LINK)
114+
setup.add_argument("--marketplace", type=Path, default=DEFAULT_MARKETPLACE_PATH)
115+
setup.add_argument(
116+
"--python",
117+
type=Path,
118+
default=None,
119+
dest="python_executable",
120+
help="Python executable Codex should use for the MCP server.",
121+
)
122+
setup.add_argument(
123+
"--force-plugin",
124+
action="store_true",
125+
help="Replace an existing generated plugin wrapper or source-checkout symlink.",
126+
)
127+
setup.add_argument("--skip-pricing", action="store_true")
128+
setup.add_argument(
129+
"--update-pricing",
130+
action="store_true",
131+
help="Fetch current pricing during setup instead of writing a local template.",
132+
)
133+
134+
98135
def _add_doctor_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
99136
doctor = subparsers.add_parser("doctor", help="Check local setup without writing files")
100137
doctor.add_argument("--json", action="store_true", dest="as_json")
@@ -128,6 +165,35 @@ def _add_install_plugin_parser(
128165
)
129166

130167

168+
def _add_upgrade_plugin_parser(
169+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
170+
) -> None:
171+
upgrade_plugin_cmd = subparsers.add_parser(
172+
"upgrade-plugin",
173+
help="Refresh the generated local Codex plugin wrapper for this installed package",
174+
)
175+
upgrade_plugin_cmd.add_argument("--plugin-dir", type=Path, default=DEFAULT_PLUGIN_LINK)
176+
upgrade_plugin_cmd.add_argument("--marketplace", type=Path, default=DEFAULT_MARKETPLACE_PATH)
177+
upgrade_plugin_cmd.add_argument(
178+
"--python",
179+
type=Path,
180+
default=None,
181+
dest="python_executable",
182+
help="Python executable Codex should use for the MCP server.",
183+
)
184+
185+
186+
def _add_uninstall_plugin_parser(
187+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
188+
) -> None:
189+
uninstall_plugin_cmd = subparsers.add_parser(
190+
"uninstall-plugin",
191+
help="Remove the generated local Codex plugin wrapper and marketplace entry",
192+
)
193+
uninstall_plugin_cmd.add_argument("--plugin-dir", type=Path, default=DEFAULT_PLUGIN_LINK)
194+
uninstall_plugin_cmd.add_argument("--marketplace", type=Path, default=DEFAULT_MARKETPLACE_PATH)
195+
196+
131197
def _add_refresh_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
132198
refresh = subparsers.add_parser("refresh", help="Scan Codex logs into SQLite")
133199
refresh.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
@@ -155,6 +221,18 @@ def _add_rebuild_index_parser(
155221
rebuild.add_argument("--include-archived", action="store_true")
156222

157223

224+
def _add_reset_db_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
225+
reset = subparsers.add_parser(
226+
"reset-db",
227+
help="Clear tracker-owned aggregate rows and refresh metadata",
228+
)
229+
reset.add_argument(
230+
"--yes",
231+
action="store_true",
232+
help="Confirm clearing local aggregate usage rows. Raw Codex logs are not touched.",
233+
)
234+
235+
158236
def _add_summary_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
159237
summary = subparsers.add_parser("summary", help="Show aggregate usage summary")
160238
summary.add_argument(
@@ -299,6 +377,70 @@ def _add_allowance_parser(
299377
allowance.add_argument("--force", action="store_true")
300378

301379

380+
def _add_support_bundle_parser(
381+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
382+
) -> None:
383+
support = subparsers.add_parser(
384+
"support-bundle",
385+
help="Write a privacy-preserving diagnostic bundle for support",
386+
)
387+
support.add_argument("--output", type=Path, default=DEFAULT_SUPPORT_BUNDLE_PATH)
388+
support.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
389+
390+
391+
def _run_setup(args: argparse.Namespace) -> int:
392+
lines = ["Codex Usage Tracker setup summary", ""]
393+
lines.append(
394+
f"Codex home: {args.codex_home.expanduser()} "
395+
f"({'found' if args.codex_home.expanduser().exists() else 'not found yet'})"
396+
)
397+
install_result = install_plugin(
398+
plugin_dir=args.plugin_dir,
399+
marketplace_path=args.marketplace,
400+
python_executable=args.python_executable,
401+
force=args.force_plugin,
402+
)
403+
lines.append(f"Plugin: installed at {install_result.plugin_dir}")
404+
lines.append(f"MCP Python: {install_result.python_executable}")
405+
if args.skip_pricing:
406+
lines.append("Pricing: skipped")
407+
elif args.update_pricing:
408+
pricing_result = update_pricing_from_openai_docs(args.pricing)
409+
lines.append(
410+
f"Pricing: updated {pricing_result.model_count} entries from {pricing_result.source_url}"
411+
)
412+
elif args.pricing.expanduser().exists():
413+
lines.append(f"Pricing: existing config at {args.pricing}")
414+
else:
415+
pricing_output = write_pricing_template(args.pricing)
416+
lines.append(f"Pricing: wrote local template at {pricing_output}")
417+
refresh_result = refresh_usage_index(
418+
codex_home=args.codex_home,
419+
db_path=args.db,
420+
include_archived=args.include_archived,
421+
)
422+
lines.append(
423+
f"Refresh: scanned {refresh_result.scanned_files} files, parsed "
424+
f"{refresh_result.parsed_events} events, skipped {refresh_result.skipped_events}"
425+
)
426+
doctor_report = run_doctor(
427+
codex_home=args.codex_home,
428+
db_path=args.db,
429+
pricing_path=args.pricing,
430+
plugin_link=args.plugin_dir,
431+
marketplace_path=args.marketplace,
432+
suggest_repair=True,
433+
)
434+
lines.append(f"Doctor: {doctor_report['status']}")
435+
if doctor_report.get("repair_suggestions"):
436+
lines.append("Repair suggestions:")
437+
lines.extend(f"- {suggestion}" for suggestion in doctor_report["repair_suggestions"])
438+
lines.append("")
439+
lines.append("Restart Codex to discover or refresh the plugin tools.")
440+
print("\n".join(lines))
441+
return 0 if doctor_report["status"] != "fail" else 1
442+
443+
302444
def _run_doctor(args: argparse.Namespace) -> int:
303445
report = run_doctor(
304446
db_path=args.db,
@@ -324,6 +466,37 @@ def _run_install_plugin(args: argparse.Namespace) -> int:
324466
return 0
325467

326468

469+
def _run_upgrade_plugin(args: argparse.Namespace) -> int:
470+
result = install_plugin(
471+
plugin_dir=args.plugin_dir,
472+
marketplace_path=args.marketplace,
473+
python_executable=args.python_executable,
474+
force=True,
475+
)
476+
print(f"Upgraded Codex Usage Tracker plugin at {result.plugin_dir}.")
477+
print(f"MCP Python: {result.python_executable}")
478+
print(f"Updated marketplace: {result.marketplace_path}")
479+
print("Restart Codex to discover the refreshed plugin.")
480+
return 0
481+
482+
483+
def _run_uninstall_plugin(args: argparse.Namespace) -> int:
484+
result = uninstall_plugin(
485+
plugin_dir=args.plugin_dir,
486+
marketplace_path=args.marketplace,
487+
)
488+
print(
489+
f"Removed plugin path: {'yes' if result.removed_plugin_path else 'already absent'} "
490+
f"({result.plugin_dir})"
491+
)
492+
print(
493+
f"Removed marketplace entry: {'yes' if result.removed_marketplace_entry else 'not present'} "
494+
f"({result.marketplace_path})"
495+
)
496+
print("Restart Codex to unload plugin tools from new sessions.")
497+
return 0
498+
499+
327500
def _run_refresh(args: argparse.Namespace) -> int:
328501
result = refresh_usage_index(
329502
codex_home=args.codex_home,
@@ -389,6 +562,19 @@ def _run_rebuild_index(args: argparse.Namespace) -> int:
389562
return 0
390563

391564

565+
def _run_reset_db(args: argparse.Namespace) -> int:
566+
if not args.yes:
567+
raise ValueError(
568+
"reset-db clears local aggregate usage rows. Re-run with --yes to confirm."
569+
)
570+
result = reset_usage_database(db_path=args.db)
571+
print(
572+
f"Cleared {result['deleted_usage_events']} aggregate usage rows from {result['db_path']}."
573+
)
574+
print("Raw Codex logs were not touched.")
575+
return 0
576+
577+
392578
def _run_summary(args: argparse.Namespace) -> int:
393579
report = build_summary_report(
394580
db_path=args.db,
@@ -536,12 +722,29 @@ def _run_init_allowance(args: argparse.Namespace) -> int:
536722
return 0
537723

538724

725+
def _run_support_bundle(args: argparse.Namespace) -> int:
726+
output = build_support_bundle(
727+
output_path=args.output,
728+
codex_home=args.codex_home,
729+
db_path=args.db,
730+
pricing_path=args.pricing,
731+
allowance_path=args.allowance,
732+
)
733+
print(f"Wrote privacy-preserving support bundle to {output}")
734+
print("Bundle excludes raw logs, prompts, assistant messages, tool output, and context text.")
735+
return 0
736+
737+
539738
_COMMAND_HANDLERS = {
739+
"setup": _run_setup,
540740
"doctor": _run_doctor,
541741
"install-plugin": _run_install_plugin,
742+
"upgrade-plugin": _run_upgrade_plugin,
743+
"uninstall-plugin": _run_uninstall_plugin,
542744
"refresh": _run_refresh,
543745
"inspect-log": _run_inspect_log,
544746
"rebuild-index": _run_rebuild_index,
747+
"reset-db": _run_reset_db,
545748
"summary": _run_summary,
546749
"session": _run_session,
547750
"context": _run_context,
@@ -554,6 +757,7 @@ def _run_init_allowance(args: argparse.Namespace) -> int:
554757
"init-pricing": _run_init_pricing,
555758
"update-pricing": _run_update_pricing,
556759
"init-allowance": _run_init_allowance,
760+
"support-bundle": _run_support_bundle,
557761
}
558762

559763
if __name__ == "__main__":

src/codex_usage_tracker/paths.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
APP_DIR = Path.home() / ".codex-usage-tracker"
99
DEFAULT_DB_PATH = APP_DIR / "usage.sqlite3"
1010
DEFAULT_DASHBOARD_PATH = APP_DIR / "dashboard.html"
11+
DEFAULT_SUPPORT_BUNDLE_PATH = APP_DIR / "support-bundle.json"
1112
DEFAULT_PRICING_PATH = APP_DIR / "pricing.json"
1213
DEFAULT_ALLOWANCE_PATH = APP_DIR / "allowance.json"
1314
DEFAULT_CODEX_HOME = Path.home() / ".codex"

0 commit comments

Comments
 (0)