2525 DEFAULT_MARKETPLACE_PATH ,
2626 DEFAULT_PLUGIN_LINK ,
2727 DEFAULT_PRICING_PATH ,
28+ DEFAULT_SUPPORT_BUNDLE_PATH ,
2829)
2930from 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
3132from codex_usage_tracker .pricing import (
3233 OPENAI_PRICING_MD_URL ,
3334 VALID_PRICING_TIERS ,
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
5154from 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+
98135def _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+
131197def _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+
158236def _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+
302444def _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+
327500def _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+
392578def _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
559763if __name__ == "__main__" :
0 commit comments