klaude-code 2.8.0__tar.gz → 2.9.0__tar.gz

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 (265) hide show
  1. {klaude_code-2.8.0 → klaude_code-2.9.0}/PKG-INFO +3 -6
  2. {klaude_code-2.8.0 → klaude_code-2.9.0}/README.md +1 -5
  3. {klaude_code-2.8.0 → klaude_code-2.9.0}/pyproject.toml +2 -1
  4. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/app/runtime.py +2 -1
  5. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/antigravity/oauth.py +0 -9
  6. klaude_code-2.9.0/src/klaude_code/auth/antigravity/token_manager.py +27 -0
  7. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/base.py +53 -0
  8. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/codex/exceptions.py +0 -4
  9. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/codex/oauth.py +32 -28
  10. klaude_code-2.9.0/src/klaude_code/auth/codex/token_manager.py +26 -0
  11. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/cost_cmd.py +128 -39
  12. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/list_model.py +27 -10
  13. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/main.py +15 -4
  14. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/assets/builtin_config.yaml +8 -24
  15. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/config.py +47 -25
  16. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/sub_agent_model_helper.py +18 -13
  17. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/thinking.py +0 -8
  18. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/const.py +2 -2
  19. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/agent_profile.py +11 -53
  20. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/compaction/compaction.py +4 -6
  21. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/compaction/overflow.py +0 -4
  22. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/executor.py +51 -5
  23. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/manager/llm_clients.py +9 -1
  24. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-claude-code.md +4 -4
  25. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/reminders.py +21 -23
  26. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/task.py +0 -4
  27. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/__init__.py +3 -2
  28. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/apply_patch.py +0 -27
  29. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/edit_tool.py +1 -2
  30. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/read_tool.md +3 -2
  31. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/read_tool.py +15 -2
  32. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/offload.py +0 -35
  33. klaude_code-2.9.0/src/klaude_code/core/tool/sub_agent/__init__.py +6 -0
  34. klaude_code-2.9.0/src/klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  35. klaude_code-2.9.0/src/klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  36. klaude_code-2.9.0/src/klaude_code/core/tool/sub_agent/task.md +20 -0
  37. klaude_code-2.9.0/src/klaude_code/core/tool/sub_agent/task.py +205 -0
  38. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/tool_registry.py +0 -16
  39. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/turn.py +1 -1
  40. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/anthropic/input.py +6 -5
  41. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/antigravity/input.py +14 -7
  42. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/codex/client.py +22 -0
  43. klaude_code-2.9.0/src/klaude_code/llm/codex/prompt_sync.py +237 -0
  44. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/google/client.py +8 -6
  45. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/google/input.py +20 -12
  46. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/image.py +18 -11
  47. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/input_common.py +14 -6
  48. klaude_code-2.9.0/src/klaude_code/llm/json_stable.py +37 -0
  49. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openai_compatible/input.py +0 -10
  50. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openai_compatible/stream.py +16 -1
  51. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/registry.py +0 -5
  52. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/responses/input.py +15 -5
  53. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/usage.py +0 -8
  54. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/commands.py +1 -0
  55. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/events.py +2 -1
  56. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/message.py +2 -2
  57. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/model.py +20 -1
  58. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/op.py +27 -0
  59. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/op_handler.py +10 -0
  60. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  61. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/sub_agent/__init__.py +13 -34
  62. klaude_code-2.9.0/src/klaude_code/protocol/sub_agent/explore.py +21 -0
  63. klaude_code-2.9.0/src/klaude_code/protocol/sub_agent/image_gen.py +38 -0
  64. klaude_code-2.9.0/src/klaude_code/protocol/sub_agent/task.py +17 -0
  65. klaude_code-2.9.0/src/klaude_code/protocol/sub_agent/web.py +21 -0
  66. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/tools.py +2 -0
  67. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/export.py +308 -299
  68. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/session.py +58 -21
  69. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/store.py +0 -4
  70. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/templates/export_session.html +430 -134
  71. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/assets/deslop/SKILL.md +9 -0
  72. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/system_skills.py +0 -20
  73. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/__init__.py +3 -0
  74. klaude_code-2.9.0/src/klaude_code/tui/command/continue_cmd.py +34 -0
  75. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/fork_session_cmd.py +5 -2
  76. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/resume_cmd.py +9 -2
  77. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  78. klaude_code-2.9.0/src/klaude_code/tui/components/assistant.py +2 -0
  79. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/command_output.py +3 -1
  80. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/developer.py +3 -0
  81. klaude_code-2.9.0/src/klaude_code/tui/components/diffs.py +90 -0
  82. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/errors.py +4 -0
  83. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/mermaid_viewer.py +2 -2
  84. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/markdown.py +60 -63
  85. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/theme.py +2 -0
  86. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/sub_agent.py +2 -46
  87. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/thinking.py +0 -33
  88. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/tools.py +43 -21
  89. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/images.py +21 -18
  90. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/key_bindings.py +2 -2
  91. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/prompt_toolkit.py +49 -49
  92. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/machine.py +15 -11
  93. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/renderer.py +12 -20
  94. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/runner.py +2 -1
  95. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/image.py +6 -34
  96. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/common.py +0 -70
  97. klaude_code-2.8.0/src/klaude_code/auth/antigravity/token_manager.py +0 -45
  98. klaude_code-2.8.0/src/klaude_code/auth/codex/token_manager.py +0 -44
  99. klaude_code-2.8.0/src/klaude_code/core/tool/sub_agent_tool.py +0 -126
  100. klaude_code-2.8.0/src/klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  101. klaude_code-2.8.0/src/klaude_code/protocol/sub_agent/explore.py +0 -48
  102. klaude_code-2.8.0/src/klaude_code/protocol/sub_agent/image_gen.py +0 -109
  103. klaude_code-2.8.0/src/klaude_code/protocol/sub_agent/task.py +0 -61
  104. klaude_code-2.8.0/src/klaude_code/protocol/sub_agent/web.py +0 -65
  105. klaude_code-2.8.0/src/klaude_code/tui/components/assistant.py +0 -28
  106. klaude_code-2.8.0/src/klaude_code/tui/components/diffs.py +0 -296
  107. klaude_code-2.8.0/src/klaude_code/tui/components/rich/searchable_text.py +0 -68
  108. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/.DS_Store +0 -0
  109. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/__init__.py +0 -0
  110. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/app/__init__.py +0 -0
  111. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/AGENTS.md +0 -0
  112. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/__init__.py +0 -0
  113. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/antigravity/__init__.py +0 -0
  114. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/antigravity/exceptions.py +0 -0
  115. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/antigravity/pkce.py +0 -0
  116. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/claude/__init__.py +0 -0
  117. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/claude/exceptions.py +0 -0
  118. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/claude/oauth.py +0 -0
  119. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/claude/token_manager.py +0 -0
  120. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/codex/__init__.py +0 -0
  121. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/codex/jwt_utils.py +0 -0
  122. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/auth/env.py +0 -0
  123. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/__init__.py +0 -0
  124. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/auth_cmd.py +0 -0
  125. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/config_cmd.py +0 -0
  126. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/debug.py +0 -0
  127. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/cli/self_update.py +0 -0
  128. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/__init__.py +0 -0
  129. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/assets/__init__.py +0 -0
  130. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/builtin_config.py +0 -0
  131. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/config/model_matcher.py +0 -0
  132. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/__init__.py +0 -0
  133. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/agent.py +0 -0
  134. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/compaction/AGENTS.md +0 -0
  135. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/compaction/__init__.py +0 -0
  136. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/compaction/prompts.py +0 -0
  137. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/loaded_skills.py +0 -0
  138. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/manager/__init__.py +0 -0
  139. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/manager/llm_clients_builder.py +0 -0
  140. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/manager/sub_agent_manager.py +0 -0
  141. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-antigravity.md +0 -0
  142. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +0 -0
  143. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-2.md +0 -0
  144. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-codex.md +0 -0
  145. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-gemini.md +0 -0
  146. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-minimal.md +0 -0
  147. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-sub-agent-explore.md +0 -0
  148. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-sub-agent-image-gen.md +0 -0
  149. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-sub-agent-web.md +0 -0
  150. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/prompts/prompt-sub-agent.md +0 -0
  151. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/context.py +0 -0
  152. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/__init__.py +0 -0
  153. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/_utils.py +0 -0
  154. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/apply_patch_tool.md +0 -0
  155. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/apply_patch_tool.py +0 -0
  156. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/diff_builder.py +0 -0
  157. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/edit_tool.md +0 -0
  158. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/write_tool.md +0 -0
  159. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/file/write_tool.py +0 -0
  160. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/report_back_tool.py +0 -0
  161. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/shell/__init__.py +0 -0
  162. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/shell/bash_tool.md +0 -0
  163. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/shell/bash_tool.py +0 -0
  164. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/shell/command_safety.py +0 -0
  165. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/todo/__init__.py +0 -0
  166. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/todo/todo_write_tool.md +0 -0
  167. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/todo/todo_write_tool.py +0 -0
  168. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/todo/todo_write_tool_raw.md +0 -0
  169. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/todo/update_plan_tool.md +0 -0
  170. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/todo/update_plan_tool.py +0 -0
  171. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/tool_abc.py +0 -0
  172. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/tool_runner.py +0 -0
  173. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/__init__.py +0 -0
  174. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/mermaid_tool.md +0 -0
  175. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/mermaid_tool.py +0 -0
  176. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/web_fetch_tool.md +0 -0
  177. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/web_fetch_tool.py +0 -0
  178. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/web_search_tool.md +0 -0
  179. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/core/tool/web/web_search_tool.py +0 -0
  180. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/__init__.py +0 -0
  181. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/anthropic/__init__.py +0 -0
  182. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/anthropic/client.py +0 -0
  183. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/antigravity/__init__.py +0 -0
  184. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/antigravity/client.py +0 -0
  185. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/bedrock/__init__.py +0 -0
  186. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/bedrock/client.py +0 -0
  187. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/claude/__init__.py +0 -0
  188. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/claude/client.py +0 -0
  189. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/client.py +0 -0
  190. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/codex/__init__.py +0 -0
  191. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/google/__init__.py +0 -0
  192. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openai_compatible/__init__.py +0 -0
  193. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openai_compatible/client.py +0 -0
  194. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openrouter/__init__.py +0 -0
  195. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openrouter/client.py +0 -0
  196. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openrouter/input.py +0 -0
  197. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/openrouter/reasoning.py +0 -0
  198. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/partial_message.py +0 -0
  199. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/responses/__init__.py +0 -0
  200. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/responses/client.py +0 -0
  201. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/llm/stream_parts.py +0 -0
  202. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/log.py +0 -0
  203. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/__init__.py +0 -0
  204. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/protocol/llm_param.py +0 -0
  205. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/__init__.py +0 -0
  206. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/codec.py +0 -0
  207. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/selector.py +0 -0
  208. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/session/templates/mermaid_viewer.html +0 -0
  209. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/.DS_Store +0 -0
  210. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/__init__.py +0 -0
  211. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/assets/.DS_Store +0 -0
  212. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/assets/create-plan/SKILL.md +0 -0
  213. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/assets/handoff/SKILL.md +0 -0
  214. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/assets/skill-creator/SKILL.md +0 -0
  215. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/loader.py +0 -0
  216. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/skill/manager.py +0 -0
  217. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/__init__.py +0 -0
  218. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/clear_cmd.py +0 -0
  219. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/command_abc.py +0 -0
  220. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/compact_cmd.py +0 -0
  221. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/copy_cmd.py +0 -0
  222. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/debug_cmd.py +0 -0
  223. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/export_cmd.py +0 -0
  224. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/export_online_cmd.py +0 -0
  225. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/model_cmd.py +0 -0
  226. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/model_picker.py +0 -0
  227. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/prompt-init.md +0 -0
  228. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/prompt_command.py +0 -0
  229. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/refresh_cmd.py +0 -0
  230. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/registry.py +0 -0
  231. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/status_cmd.py +0 -0
  232. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/command/thinking_cmd.py +0 -0
  233. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/commands.py +0 -0
  234. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/__init__.py +0 -0
  235. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/bash_syntax.py +0 -0
  236. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/common.py +0 -0
  237. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/metadata.py +0 -0
  238. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/__init__.py +0 -0
  239. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/cjk_wrap.py +0 -0
  240. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/code_panel.py +0 -0
  241. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/live.py +0 -0
  242. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/quote.py +0 -0
  243. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/rich/status.py +0 -0
  244. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/user_input.py +0 -0
  245. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/components/welcome.py +0 -0
  246. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/display.py +0 -0
  247. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/AGENTS.md +0 -0
  248. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/__init__.py +0 -0
  249. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/completers.py +0 -0
  250. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/drag_drop.py +0 -0
  251. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/input/paste.py +0 -0
  252. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/__init__.py +0 -0
  253. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/color.py +0 -0
  254. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/control.py +0 -0
  255. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/notifier.py +0 -0
  256. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/progress_bar.py +0 -0
  257. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/tui/terminal/selector.py +0 -0
  258. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/__init__.py +0 -0
  259. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/core/__init__.py +0 -0
  260. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/core/display.py +0 -0
  261. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/core/input.py +0 -0
  262. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/debug_mode.py +0 -0
  263. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/terminal/__init__.py +0 -0
  264. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/ui/terminal/title.py +0 -0
  265. {klaude_code-2.8.0 → klaude_code-2.9.0}/src/klaude_code/update.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
7
7
  Requires-Dist: ddgs>=9.9.3
8
8
  Requires-Dist: diff-match-patch>=20241021
9
+ Requires-Dist: filelock>=3.20.3
9
10
  Requires-Dist: google-genai>=1.56.0
10
11
  Requires-Dist: markdown-it-py>=4.0.0
11
12
  Requires-Dist: openai>=1.102.0
@@ -23,7 +24,7 @@ Description-Content-Type: text/markdown
23
24
  Minimal code agent CLI.
24
25
 
25
26
  ## Features
26
- - **Multi-provider**: Anthropic Message API, OpenAI Responses API, OpenRouter, Claude Max OAuth and ChatGPT Codex OAuth etc.
27
+ - **Multi-provider**: Anthropic Message API, OpenAI Responses API, OpenRouter, ChatGPT Codex OAuth etc.
27
28
  - **Keep reasoning item in context**: Interleaved thinking support
28
29
  - **Model-aware tools**: Claude Code tool set for Opus, `apply_patch` for GPT-5/Codex
29
30
  - **Reminders**: Cooldown-based todo tracking, instruction reinforcement and external file change reminder
@@ -107,7 +108,6 @@ On first run, you'll be prompted to select a model. Your choice is saved as `mai
107
108
  | Provider | Env Variable | Models |
108
109
  |-------------|-----------------------|-------------------------------------------------------------------------------|
109
110
  | anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
110
- | claude | N/A (OAuth) | sonnet@claude, opus@claude (requires Claude Pro/Max subscription) |
111
111
  | openai | `OPENAI_API_KEY` | gpt-5.2 |
112
112
  | openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
113
113
  | deepseek | `DEEPSEEK_API_KEY` | deepseek |
@@ -139,7 +139,6 @@ klaude auth login deepseek # Set DEEPSEEK_API_KEY
139
139
  klaude auth login moonshot # Set MOONSHOT_API_KEY
140
140
 
141
141
  # OAuth login for subscription-based providers
142
- klaude auth login claude # Claude Pro/Max subscription
143
142
  klaude auth login codex # ChatGPT Pro subscription
144
143
  ```
145
144
 
@@ -148,7 +147,6 @@ API keys are stored in `~/.klaude/klaude-auth.json` and used as fallback when en
148
147
  To logout from OAuth providers:
149
148
 
150
149
  ```bash
151
- klaude auth logout claude
152
150
  klaude auth logout codex
153
151
  ```
154
152
 
@@ -201,7 +199,6 @@ provider_list:
201
199
  ##### Supported Protocols
202
200
 
203
201
  - `anthropic` - Anthropic Messages API
204
- - `claude_oauth` - Claude OAuth (for Claude Pro/Max subscribers)
205
202
  - `openai` - OpenAI Chat Completion API
206
203
  - `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
207
204
  - `codex_oauth` - OpenAI Codex CLI (OAuth-based, for ChatGPT Pro subscribers)
@@ -3,7 +3,7 @@
3
3
  Minimal code agent CLI.
4
4
 
5
5
  ## Features
6
- - **Multi-provider**: Anthropic Message API, OpenAI Responses API, OpenRouter, Claude Max OAuth and ChatGPT Codex OAuth etc.
6
+ - **Multi-provider**: Anthropic Message API, OpenAI Responses API, OpenRouter, ChatGPT Codex OAuth etc.
7
7
  - **Keep reasoning item in context**: Interleaved thinking support
8
8
  - **Model-aware tools**: Claude Code tool set for Opus, `apply_patch` for GPT-5/Codex
9
9
  - **Reminders**: Cooldown-based todo tracking, instruction reinforcement and external file change reminder
@@ -87,7 +87,6 @@ On first run, you'll be prompted to select a model. Your choice is saved as `mai
87
87
  | Provider | Env Variable | Models |
88
88
  |-------------|-----------------------|-------------------------------------------------------------------------------|
89
89
  | anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
90
- | claude | N/A (OAuth) | sonnet@claude, opus@claude (requires Claude Pro/Max subscription) |
91
90
  | openai | `OPENAI_API_KEY` | gpt-5.2 |
92
91
  | openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
93
92
  | deepseek | `DEEPSEEK_API_KEY` | deepseek |
@@ -119,7 +118,6 @@ klaude auth login deepseek # Set DEEPSEEK_API_KEY
119
118
  klaude auth login moonshot # Set MOONSHOT_API_KEY
120
119
 
121
120
  # OAuth login for subscription-based providers
122
- klaude auth login claude # Claude Pro/Max subscription
123
121
  klaude auth login codex # ChatGPT Pro subscription
124
122
  ```
125
123
 
@@ -128,7 +126,6 @@ API keys are stored in `~/.klaude/klaude-auth.json` and used as fallback when en
128
126
  To logout from OAuth providers:
129
127
 
130
128
  ```bash
131
- klaude auth logout claude
132
129
  klaude auth logout codex
133
130
  ```
134
131
 
@@ -181,7 +178,6 @@ provider_list:
181
178
  ##### Supported Protocols
182
179
 
183
180
  - `anthropic` - Anthropic Messages API
184
- - `claude_oauth` - Claude OAuth (for Claude Pro/Max subscribers)
185
181
  - `openai` - OpenAI Chat Completion API
186
182
  - `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
187
183
  - `codex_oauth` - OpenAI Codex CLI (OAuth-based, for ChatGPT Pro subscribers)
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "klaude-code"
7
- version = "2.8.0"
7
+ version = "2.9.0"
8
8
  description = "Minimal code agent CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "chardet>=5.2.0",
14
14
  "ddgs>=9.9.3",
15
15
  "diff-match-patch>=20241021",
16
+ "filelock>=3.20.3",
16
17
  "google-genai>=1.56.0",
17
18
  "markdown-it-py>=4.0.0",
18
19
  "openai>=1.102.0",
@@ -178,6 +178,7 @@ async def handle_keyboard_interrupt(executor: Executor) -> None:
178
178
  log("Bye!")
179
179
  session_id = executor.context.current_session_id()
180
180
  if session_id and Session.exists(session_id):
181
- log(("Resume with:", "dim"), (f"klaude --resume {session_id}", "green"))
181
+ short_id = Session.shortest_unique_prefix(session_id)
182
+ log(("Resume with:", "dim"), (f"klaude -r {short_id}", "green"))
182
183
  with contextlib.suppress(Exception):
183
184
  await executor.submit(op.InterruptOperation(target_session_id=None))
@@ -309,12 +309,3 @@ class AntigravityOAuth:
309
309
  state = self.refresh()
310
310
 
311
311
  return state.access_token, state.project_id
312
-
313
- def get_api_key_json(self) -> str:
314
- """Get API key as JSON string for LLM client.
315
-
316
- Returns:
317
- JSON string with token and projectId.
318
- """
319
- access_token, project_id = self.ensure_valid_token()
320
- return json.dumps({"token": access_token, "projectId": project_id})
@@ -0,0 +1,27 @@
1
+ """Token storage and management for Antigravity authentication."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from klaude_code.auth.base import BaseAuthState, BaseTokenManager
7
+
8
+
9
+ class AntigravityAuthState(BaseAuthState):
10
+ """Stored authentication state for Antigravity."""
11
+
12
+ project_id: str
13
+ email: str | None = None
14
+
15
+
16
+ class AntigravityTokenManager(BaseTokenManager[AntigravityAuthState]):
17
+ """Manage Antigravity OAuth tokens."""
18
+
19
+ def __init__(self, auth_file: Path | None = None):
20
+ super().__init__(auth_file)
21
+
22
+ @property
23
+ def storage_key(self) -> str:
24
+ return "antigravity"
25
+
26
+ def _create_state(self, data: dict[str, Any]) -> AntigravityAuthState:
27
+ return AntigravityAuthState.model_validate(data)
@@ -3,12 +3,15 @@
3
3
  import json
4
4
  import time
5
5
  from abc import ABC, abstractmethod
6
+ from collections.abc import Callable
6
7
  from pathlib import Path
7
8
  from typing import Any, cast
8
9
 
10
+ from filelock import FileLock, Timeout
9
11
  from pydantic import BaseModel
10
12
 
11
13
  KLAUDE_AUTH_FILE = Path.home() / ".klaude" / "klaude-auth.json"
14
+ LOCK_TIMEOUT_SECONDS = 30 # Maximum time to wait for lock acquisition
12
15
 
13
16
 
14
17
  class BaseAuthState(BaseModel):
@@ -99,3 +102,53 @@ class BaseTokenManager[T: BaseAuthState](ABC):
99
102
  def clear_cached_state(self) -> None:
100
103
  """Clear in-memory cached state to force reload from file on next access."""
101
104
  self._state = None
105
+
106
+ def _get_lock_file(self) -> Path:
107
+ """Get the lock file path for this auth file."""
108
+ return self.auth_file.with_suffix(".lock")
109
+
110
+ def refresh_with_lock(self, refresh_fn: Callable[[T], T]) -> T:
111
+ """Refresh token with file locking to prevent concurrent refresh.
112
+
113
+ This prevents multiple instances from simultaneously refreshing the same token.
114
+ If another instance has already refreshed, returns the updated state.
115
+
116
+ Args:
117
+ refresh_fn: Function that takes current state and returns new state.
118
+
119
+ Returns:
120
+ The new or already-refreshed authentication state.
121
+
122
+ Raises:
123
+ Timeout: If unable to acquire the lock within timeout.
124
+ ValueError: If not logged in.
125
+ """
126
+ lock_file = self._get_lock_file()
127
+ lock = FileLock(lock_file, timeout=LOCK_TIMEOUT_SECONDS)
128
+
129
+ try:
130
+ with lock:
131
+ # Re-read file after acquiring lock - another instance may have refreshed
132
+ self.clear_cached_state()
133
+ state = self.load()
134
+
135
+ if state is None:
136
+ raise ValueError(f"Not logged in to {self.storage_key}")
137
+
138
+ # Check if token is still expired after re-reading
139
+ if not state.is_expired():
140
+ # Another instance already refreshed, use their result
141
+ return state
142
+
143
+ # Token still expired, we need to refresh
144
+ new_state = refresh_fn(state)
145
+ self.save(new_state)
146
+ return new_state
147
+
148
+ except Timeout:
149
+ # Lock timeout - try to re-read file in case another instance succeeded
150
+ self.clear_cached_state()
151
+ state = self.load()
152
+ if state and not state.is_expired():
153
+ return state
154
+ raise
@@ -15,7 +15,3 @@ class CodexTokenExpiredError(CodexAuthError):
15
15
 
16
16
  class CodexOAuthError(CodexAuthError):
17
17
  """OAuth flow failed."""
18
-
19
-
20
- class CodexUnsupportedModelError(CodexAuthError):
21
- """Model is not supported by codex_oauth protocol."""
@@ -177,43 +177,47 @@ class CodexOAuth:
177
177
  )
178
178
 
179
179
  def refresh(self) -> CodexAuthState:
180
- """Refresh the access token using refresh token."""
181
- state = self.token_manager.get_state()
182
- if state is None:
183
- from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
180
+ """Refresh the access token using refresh token with file locking.
184
181
 
185
- raise CodexNotLoggedInError("Not logged in to Codex. Run 'klaude login codex' first.")
182
+ Uses file locking to prevent multiple instances from refreshing simultaneously.
183
+ If another instance has already refreshed, returns the updated state.
184
+ """
186
185
 
187
- data = {
188
- "grant_type": "refresh_token",
189
- "client_id": CLIENT_ID,
190
- "refresh_token": state.refresh_token,
191
- }
186
+ def do_refresh(current_state: CodexAuthState) -> CodexAuthState:
187
+ data = {
188
+ "grant_type": "refresh_token",
189
+ "client_id": CLIENT_ID,
190
+ "refresh_token": current_state.refresh_token,
191
+ }
192
192
 
193
- with httpx.Client() as client:
194
- response = client.post(TOKEN_URL, data=data)
193
+ with httpx.Client() as client:
194
+ response = client.post(TOKEN_URL, data=data)
195
195
 
196
- if response.status_code != 200:
197
- from klaude_code.auth.codex.exceptions import CodexTokenExpiredError
196
+ if response.status_code != 200:
197
+ from klaude_code.auth.codex.exceptions import CodexTokenExpiredError
198
198
 
199
- raise CodexTokenExpiredError(f"Token refresh failed: {response.text}")
199
+ raise CodexTokenExpiredError(f"Token refresh failed: {response.text}")
200
200
 
201
- tokens = response.json()
202
- access_token = tokens["access_token"]
203
- refresh_token = tokens.get("refresh_token", state.refresh_token)
204
- expires_in = tokens.get("expires_in", 3600)
201
+ tokens = response.json()
202
+ access_token = tokens["access_token"]
203
+ refresh_token = tokens.get("refresh_token", current_state.refresh_token)
204
+ expires_in = tokens.get("expires_in", 3600)
205
205
 
206
- account_id = extract_account_id(access_token)
206
+ account_id = extract_account_id(access_token)
207
207
 
208
- new_state = CodexAuthState(
209
- access_token=access_token,
210
- refresh_token=refresh_token,
211
- expires_at=int(time.time()) + expires_in,
212
- account_id=account_id,
213
- )
208
+ return CodexAuthState(
209
+ access_token=access_token,
210
+ refresh_token=refresh_token,
211
+ expires_at=int(time.time()) + expires_in,
212
+ account_id=account_id,
213
+ )
214
+
215
+ try:
216
+ return self.token_manager.refresh_with_lock(do_refresh)
217
+ except ValueError as e:
218
+ from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
214
219
 
215
- self.token_manager.save(new_state)
216
- return new_state
220
+ raise CodexNotLoggedInError(str(e)) from e
217
221
 
218
222
  def ensure_valid_token(self) -> str:
219
223
  """Ensure we have a valid access token, refreshing if needed."""
@@ -0,0 +1,26 @@
1
+ """Token storage and management for Codex authentication."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from klaude_code.auth.base import BaseAuthState, BaseTokenManager
7
+
8
+
9
+ class CodexAuthState(BaseAuthState):
10
+ """Stored authentication state for Codex."""
11
+
12
+ account_id: str
13
+
14
+
15
+ class CodexTokenManager(BaseTokenManager[CodexAuthState]):
16
+ """Manage Codex OAuth tokens."""
17
+
18
+ def __init__(self, auth_file: Path | None = None):
19
+ super().__init__(auth_file)
20
+
21
+ @property
22
+ def storage_key(self) -> str:
23
+ return "codex"
24
+
25
+ def _create_state(self, data: dict[str, Any]) -> CodexAuthState:
26
+ return CodexAuthState.model_validate(data)
@@ -34,6 +34,16 @@ class ModelUsageStats:
34
34
  def total_tokens(self) -> int:
35
35
  return self.input_tokens + self.output_tokens
36
36
 
37
+ @property
38
+ def non_cached_input_tokens(self) -> int:
39
+ """Non-cached prompt tokens.
40
+
41
+ We store `input_tokens` as the provider-reported prompt token count, which
42
+ includes cached tokens for providers that support prompt caching.
43
+ """
44
+
45
+ return max(0, self.input_tokens - self.cached_tokens)
46
+
37
47
  def add_usage(self, usage: model.Usage) -> None:
38
48
  self.input_tokens += usage.input_tokens
39
49
  self.output_tokens += usage.output_tokens
@@ -48,41 +58,99 @@ class ModelUsageStats:
48
58
  ModelKey = tuple[str, str] # (model_name, provider)
49
59
 
50
60
 
51
- def group_models_by_provider(
52
- models: dict[ModelKey, ModelUsageStats],
53
- ) -> tuple[dict[str, list[ModelUsageStats]], dict[str, ModelUsageStats]]:
54
- """Group models by provider and compute provider totals.
61
+ @dataclass
62
+ class SubProviderGroup:
63
+ """Group of models under a sub-provider."""
64
+
65
+ name: str
66
+ models: list[ModelUsageStats]
67
+ total: ModelUsageStats
68
+
69
+
70
+ @dataclass
71
+ class ProviderGroup:
72
+ """Group of models/sub-providers under a top-level provider."""
73
+
74
+ name: str
75
+ sub_providers: dict[str, SubProviderGroup] # empty if no sub-providers
76
+ models: list[ModelUsageStats] # direct models (when no sub-provider)
77
+ total: ModelUsageStats
78
+
55
79
 
56
- Returns (models_by_provider, provider_totals) where both are sorted by cost desc.
80
+ def _sort_by_cost(stats: ModelUsageStats) -> tuple[float, float]:
81
+ return (-stats.cost_usd, -stats.cost_cny)
82
+
83
+
84
+ def group_models_by_provider(models: dict[ModelKey, ModelUsageStats]) -> dict[str, ProviderGroup]:
85
+ """Group models by provider with three-level hierarchy.
86
+
87
+ Provider strings like "openrouter/Anthropic" are split into:
88
+ - Top-level: "openrouter"
89
+ - Sub-provider: "Anthropic"
90
+
91
+ Returns dict of ProviderGroup sorted by cost desc.
57
92
  """
58
- models_by_provider: dict[str, list[ModelUsageStats]] = {}
59
- provider_totals: dict[str, ModelUsageStats] = {}
93
+ provider_groups: dict[str, ProviderGroup] = {}
60
94
 
61
95
  for stats in models.values():
62
- provider_key = stats.provider or "(unknown)"
63
- if provider_key not in models_by_provider:
64
- models_by_provider[provider_key] = []
65
- provider_totals[provider_key] = ModelUsageStats(model_name=provider_key, provider=provider_key)
66
- models_by_provider[provider_key].append(stats)
67
- provider_totals[provider_key].input_tokens += stats.input_tokens
68
- provider_totals[provider_key].output_tokens += stats.output_tokens
69
- provider_totals[provider_key].cached_tokens += stats.cached_tokens
70
- provider_totals[provider_key].cost_usd += stats.cost_usd
71
- provider_totals[provider_key].cost_cny += stats.cost_cny
96
+ provider_raw = stats.provider or "(unknown)"
72
97
 
73
- def sort_by_cost(stats: ModelUsageStats) -> tuple[float, float]:
74
- return (-stats.cost_usd, -stats.cost_cny)
98
+ # Split provider by first "/"
99
+ if "/" in provider_raw:
100
+ parts = provider_raw.split("/", 1)
101
+ top_provider, sub_provider = parts[0], parts[1]
102
+ else:
103
+ top_provider, sub_provider = provider_raw, ""
104
+
105
+ # Initialize top-level provider group
106
+ if top_provider not in provider_groups:
107
+ provider_groups[top_provider] = ProviderGroup(
108
+ name=top_provider,
109
+ sub_providers={},
110
+ models=[],
111
+ total=ModelUsageStats(model_name=top_provider),
112
+ )
113
+
114
+ group = provider_groups[top_provider]
115
+
116
+ # Accumulate to top-level total
117
+ group.total.input_tokens += stats.input_tokens
118
+ group.total.output_tokens += stats.output_tokens
119
+ group.total.cached_tokens += stats.cached_tokens
120
+ group.total.cost_usd += stats.cost_usd
121
+ group.total.cost_cny += stats.cost_cny
122
+
123
+ if sub_provider:
124
+ # Has sub-provider, add to sub-provider group
125
+ if sub_provider not in group.sub_providers:
126
+ group.sub_providers[sub_provider] = SubProviderGroup(
127
+ name=sub_provider,
128
+ models=[],
129
+ total=ModelUsageStats(model_name=sub_provider),
130
+ )
131
+ sub_group = group.sub_providers[sub_provider]
132
+ sub_group.models.append(stats)
133
+ sub_group.total.input_tokens += stats.input_tokens
134
+ sub_group.total.output_tokens += stats.output_tokens
135
+ sub_group.total.cached_tokens += stats.cached_tokens
136
+ sub_group.total.cost_usd += stats.cost_usd
137
+ sub_group.total.cost_cny += stats.cost_cny
138
+ else:
139
+ # No sub-provider, add directly to models
140
+ group.models.append(stats)
75
141
 
76
- # Sort providers by cost, and models within each provider
77
- sorted_providers = sorted(provider_totals.keys(), key=lambda p: sort_by_cost(provider_totals[p]))
78
- for provider_key in models_by_provider:
79
- models_by_provider[provider_key].sort(key=sort_by_cost)
142
+ # Sort everything by cost
143
+ for group in provider_groups.values():
144
+ group.models.sort(key=_sort_by_cost)
145
+ for sub_group in group.sub_providers.values():
146
+ sub_group.models.sort(key=_sort_by_cost)
147
+ # Sort sub-providers by cost
148
+ group.sub_providers = dict(sorted(group.sub_providers.items(), key=lambda x: _sort_by_cost(x[1].total)))
80
149
 
81
- # Rebuild dicts in sorted order
82
- sorted_models_by_provider = {p: models_by_provider[p] for p in sorted_providers}
83
- sorted_provider_totals = {p: provider_totals[p] for p in sorted_providers}
150
+ # Sort top-level providers by cost
151
+ sorted_groups = dict(sorted(provider_groups.items(), key=lambda x: _sort_by_cost(x[1].total)))
84
152
 
85
- return sorted_models_by_provider, sorted_provider_totals
153
+ return sorted_groups
86
154
 
87
155
 
88
156
  @dataclass
@@ -223,8 +291,8 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
223
291
  table.add_column("Date", style="cyan")
224
292
  table.add_column("Model", overflow="ellipsis")
225
293
  table.add_column("Input", justify="right")
226
- table.add_column("Output", justify="right")
227
294
  table.add_column("Cache", justify="right")
295
+ table.add_column("Output", justify="right")
228
296
  table.add_column("Total", justify="right")
229
297
  table.add_column("USD", justify="right")
230
298
  table.add_column("CNY", justify="right")
@@ -248,9 +316,9 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
248
316
  table.add_row(
249
317
  date_label,
250
318
  model_col,
251
- fmt(format_tokens(stats.input_tokens)),
252
- fmt(format_tokens(stats.output_tokens)),
319
+ fmt(format_tokens(stats.non_cached_input_tokens)),
253
320
  fmt(format_tokens(stats.cached_tokens)),
321
+ fmt(format_tokens(stats.output_tokens)),
254
322
  fmt(format_tokens(stats.total_tokens)),
255
323
  fmt(usd_str),
256
324
  fmt(cny_str),
@@ -261,19 +329,40 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
261
329
  date_label: str = "",
262
330
  show_subtotal: bool = True,
263
331
  ) -> None:
264
- """Render models grouped by provider with tree structure."""
265
- models_by_provider, provider_totals = group_models_by_provider(models)
332
+ """Render models grouped by provider with three-level tree structure."""
333
+ provider_groups = group_models_by_provider(models)
266
334
 
267
335
  first_row = True
268
- for provider_key, provider_models in models_by_provider.items():
269
- provider_stats = provider_totals[provider_key]
270
- add_stats_row(provider_stats, date_label=date_label if first_row else "", bold=True)
336
+ for group in provider_groups.values():
337
+ # Top-level provider
338
+ add_stats_row(group.total, date_label=date_label if first_row else "", bold=True)
271
339
  first_row = False
272
340
 
273
- for i, stats in enumerate(provider_models):
274
- is_last = i == len(provider_models) - 1
275
- prefix = " └─ " if is_last else " ├─ "
276
- add_stats_row(stats, prefix=prefix)
341
+ if group.sub_providers:
342
+ # Has sub-providers: render three-level tree
343
+ sub_list = list(group.sub_providers.values())
344
+ for sub_idx, sub_group in enumerate(sub_list):
345
+ is_last_sub = sub_idx == len(sub_list) - 1
346
+ sub_prefix = " └─ " if is_last_sub else " ├─ "
347
+
348
+ # Sub-provider row
349
+ add_stats_row(sub_group.total, prefix=sub_prefix, bold=True)
350
+
351
+ # Models under sub-provider
352
+ for model_idx, stats in enumerate(sub_group.models):
353
+ is_last_model = model_idx == len(sub_group.models) - 1
354
+ # Indent based on whether sub-provider is last
355
+ if is_last_sub:
356
+ model_prefix = " └─ " if is_last_model else " ├─ "
357
+ else:
358
+ model_prefix = " │ └─ " if is_last_model else " │ ├─ "
359
+ add_stats_row(stats, prefix=model_prefix)
360
+ else:
361
+ # No sub-providers: render two-level tree (direct models)
362
+ for model_idx, stats in enumerate(group.models):
363
+ is_last_model = model_idx == len(group.models) - 1
364
+ model_prefix = " └─ " if is_last_model else " ├─ "
365
+ add_stats_row(stats, prefix=model_prefix)
277
366
 
278
367
  if show_subtotal:
279
368
  subtotal = ModelUsageStats(model_name="(subtotal)")
@@ -234,10 +234,15 @@ def _get_model_params_display(model: ModelConfig) -> list[Text]:
234
234
  return [Text("")]
235
235
 
236
236
 
237
- def _build_provider_info_panel(provider: ProviderConfig, available: bool) -> Quote:
237
+ def _build_provider_info_panel(provider: ProviderConfig, available: bool, *, disabled: bool) -> Quote:
238
238
  """Build a Quote containing provider name and information using a two-column grid."""
239
239
  # Provider name as title
240
- if available:
240
+ if disabled:
241
+ title = Text.assemble(
242
+ (provider.provider_name, ThemeKey.CONFIG_PROVIDER),
243
+ (" (Disabled)", "dim"),
244
+ )
245
+ elif available:
241
246
  title = Text(provider.provider_name, style=ThemeKey.CONFIG_PROVIDER)
242
247
  else:
243
248
  title = Text.assemble(
@@ -297,7 +302,8 @@ def _build_models_table(
297
302
  config: Config,
298
303
  ) -> Table:
299
304
  """Build a table for models under a provider."""
300
- provider_available = not provider.is_api_key_missing()
305
+ provider_disabled = provider.disabled
306
+ provider_available = (not provider_disabled) and (not provider.is_api_key_missing())
301
307
 
302
308
  def _resolve_selector(value: str | None) -> str | None:
303
309
  if not value:
@@ -334,7 +340,15 @@ def _build_models_table(
334
340
  is_last = i == model_count - 1
335
341
  prefix = " └─ " if is_last else " ├─ "
336
342
 
337
- if not provider_available:
343
+ if provider_disabled:
344
+ name = Text.assemble(
345
+ (prefix, ThemeKey.LINES),
346
+ (model.model_name, "dim strike"),
347
+ (" (provider disabled)", "dim"),
348
+ )
349
+ model_id = Text(model.model_id or "", style="dim")
350
+ params = Text("(disabled)", style="dim")
351
+ elif not provider_available:
338
352
  name = Text.assemble((prefix, ThemeKey.LINES), (model.model_name, "dim"))
339
353
  model_id = Text(model.model_id or "", style="dim")
340
354
  params = Text("(unavailable)", style="dim")
@@ -408,19 +422,22 @@ def display_models_and_providers(config: Config, *, show_all: bool = False):
408
422
  _display_agent_models_table(config, console)
409
423
  console.print()
410
424
 
411
- # Sort providers: available (api_key set) first, unavailable (api_key not set) last
412
- sorted_providers = sorted(config.provider_list, key=lambda p: (p.is_api_key_missing(), p.provider_name))
425
+ # Sort providers: enabled+available first, disabled/unavailable last
426
+ sorted_providers = sorted(
427
+ config.provider_list,
428
+ key=lambda p: (p.disabled, p.is_api_key_missing(), p.provider_name),
429
+ )
413
430
 
414
- # Filter out unavailable providers unless show_all is True
431
+ # Filter out disabled/unavailable providers unless show_all is True
415
432
  if not show_all:
416
- sorted_providers = [p for p in sorted_providers if not p.is_api_key_missing()]
433
+ sorted_providers = [p for p in sorted_providers if (not p.disabled) and (not p.is_api_key_missing())]
417
434
 
418
435
  # Display each provider with its models table
419
436
  for provider in sorted_providers:
420
- provider_available = not provider.is_api_key_missing()
437
+ provider_available = (not provider.disabled) and (not provider.is_api_key_missing())
421
438
 
422
439
  # Provider info panel
423
- provider_panel = _build_provider_info_panel(provider, provider_available)
440
+ provider_panel = _build_provider_info_panel(provider, provider_available, disabled=provider.disabled)
424
441
  console.print(provider_panel)
425
442
  console.print()
426
443