klaude-code 2.6.0__py3-none-any.whl → 2.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/AGENTS.md +325 -0
  3. klaude_code/auth/__init__.py +17 -1
  4. klaude_code/auth/antigravity/__init__.py +20 -0
  5. klaude_code/auth/antigravity/exceptions.py +17 -0
  6. klaude_code/auth/antigravity/oauth.py +320 -0
  7. klaude_code/auth/antigravity/pkce.py +25 -0
  8. klaude_code/auth/antigravity/token_manager.py +45 -0
  9. klaude_code/auth/base.py +4 -0
  10. klaude_code/auth/claude/oauth.py +29 -9
  11. klaude_code/auth/codex/exceptions.py +4 -0
  12. klaude_code/auth/env.py +19 -15
  13. klaude_code/cli/auth_cmd.py +54 -4
  14. klaude_code/cli/cost_cmd.py +83 -160
  15. klaude_code/cli/list_model.py +50 -0
  16. klaude_code/cli/main.py +99 -9
  17. klaude_code/config/assets/builtin_config.yaml +108 -0
  18. klaude_code/config/builtin_config.py +5 -11
  19. klaude_code/config/config.py +24 -10
  20. klaude_code/const.py +11 -1
  21. klaude_code/core/agent.py +5 -1
  22. klaude_code/core/agent_profile.py +28 -32
  23. klaude_code/core/compaction/AGENTS.md +112 -0
  24. klaude_code/core/compaction/__init__.py +11 -0
  25. klaude_code/core/compaction/compaction.py +707 -0
  26. klaude_code/core/compaction/overflow.py +30 -0
  27. klaude_code/core/compaction/prompts.py +97 -0
  28. klaude_code/core/executor.py +103 -2
  29. klaude_code/core/manager/llm_clients.py +5 -0
  30. klaude_code/core/manager/llm_clients_builder.py +14 -2
  31. klaude_code/core/prompts/prompt-antigravity.md +80 -0
  32. klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
  33. klaude_code/core/reminders.py +11 -7
  34. klaude_code/core/task.py +126 -0
  35. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  36. klaude_code/core/turn.py +3 -1
  37. klaude_code/llm/antigravity/__init__.py +3 -0
  38. klaude_code/llm/antigravity/client.py +558 -0
  39. klaude_code/llm/antigravity/input.py +261 -0
  40. klaude_code/llm/registry.py +1 -0
  41. klaude_code/protocol/commands.py +0 -1
  42. klaude_code/protocol/events.py +18 -0
  43. klaude_code/protocol/llm_param.py +1 -0
  44. klaude_code/protocol/message.py +23 -1
  45. klaude_code/protocol/op.py +15 -1
  46. klaude_code/protocol/op_handler.py +5 -0
  47. klaude_code/session/session.py +36 -0
  48. klaude_code/skill/assets/create-plan/SKILL.md +6 -6
  49. klaude_code/skill/loader.py +12 -13
  50. klaude_code/skill/manager.py +3 -3
  51. klaude_code/tui/command/__init__.py +4 -4
  52. klaude_code/tui/command/compact_cmd.py +32 -0
  53. klaude_code/tui/command/copy_cmd.py +1 -1
  54. klaude_code/tui/command/fork_session_cmd.py +114 -18
  55. klaude_code/tui/command/model_picker.py +5 -1
  56. klaude_code/tui/command/thinking_cmd.py +1 -1
  57. klaude_code/tui/commands.py +6 -0
  58. klaude_code/tui/components/command_output.py +1 -1
  59. klaude_code/tui/components/rich/markdown.py +117 -1
  60. klaude_code/tui/components/rich/theme.py +18 -2
  61. klaude_code/tui/components/tools.py +39 -25
  62. klaude_code/tui/components/user_input.py +39 -28
  63. klaude_code/tui/input/AGENTS.md +44 -0
  64. klaude_code/tui/input/__init__.py +5 -2
  65. klaude_code/tui/input/completers.py +10 -14
  66. klaude_code/tui/input/drag_drop.py +146 -0
  67. klaude_code/tui/input/images.py +227 -0
  68. klaude_code/tui/input/key_bindings.py +183 -19
  69. klaude_code/tui/input/paste.py +71 -0
  70. klaude_code/tui/input/prompt_toolkit.py +32 -9
  71. klaude_code/tui/machine.py +26 -1
  72. klaude_code/tui/renderer.py +67 -4
  73. klaude_code/tui/runner.py +19 -3
  74. klaude_code/tui/terminal/image.py +103 -10
  75. klaude_code/tui/terminal/selector.py +81 -7
  76. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +10 -10
  77. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +79 -61
  78. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
  79. klaude_code/tui/command/terminal_setup_cmd.py +0 -248
  80. klaude_code/tui/input/clipboard.py +0 -152
  81. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
@@ -45,12 +45,52 @@ class ModelUsageStats:
45
45
  self.cost_usd += usage.total_cost
46
46
 
47
47
 
48
+ ModelKey = tuple[str, str] # (model_name, provider)
49
+
50
+
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.
55
+
56
+ Returns (models_by_provider, provider_totals) where both are sorted by cost desc.
57
+ """
58
+ models_by_provider: dict[str, list[ModelUsageStats]] = {}
59
+ provider_totals: dict[str, ModelUsageStats] = {}
60
+
61
+ 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
72
+
73
+ def sort_by_cost(stats: ModelUsageStats) -> tuple[float, float]:
74
+ return (-stats.cost_usd, -stats.cost_cny)
75
+
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)
80
+
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}
84
+
85
+ return sorted_models_by_provider, sorted_provider_totals
86
+
87
+
48
88
  @dataclass
49
89
  class DailyStats:
50
90
  """Aggregated stats for a single day."""
51
91
 
52
92
  date: str
53
- by_model: dict[str, ModelUsageStats] = field(default_factory=lambda: dict[str, ModelUsageStats]())
93
+ by_model: dict[ModelKey, ModelUsageStats] = field(default_factory=lambda: dict[ModelKey, ModelUsageStats]())
54
94
 
55
95
  def add_task_metadata(self, meta: model.TaskMetadata, date_str: str) -> None:
56
96
  """Add a TaskMetadata to this day's stats."""
@@ -58,26 +98,14 @@ class DailyStats:
58
98
  if not meta.usage or not meta.model_name:
59
99
  return
60
100
 
61
- model_key = meta.model_name
62
101
  provider = meta.provider or meta.usage.provider or ""
102
+ model_key: ModelKey = (meta.model_name, provider)
103
+
63
104
  if model_key not in self.by_model:
64
- self.by_model[model_key] = ModelUsageStats(model_name=model_key, provider=provider)
65
- elif not self.by_model[model_key].provider and provider:
66
- self.by_model[model_key].provider = provider
105
+ self.by_model[model_key] = ModelUsageStats(model_name=meta.model_name, provider=provider)
67
106
 
68
107
  self.by_model[model_key].add_usage(meta.usage)
69
108
 
70
- def get_subtotal(self) -> ModelUsageStats:
71
- """Get subtotal across all models for this day."""
72
- subtotal = ModelUsageStats(model_name="(subtotal)")
73
- for stats in self.by_model.values():
74
- subtotal.input_tokens += stats.input_tokens
75
- subtotal.output_tokens += stats.output_tokens
76
- subtotal.cached_tokens += stats.cached_tokens
77
- subtotal.cost_usd += stats.cost_usd
78
- subtotal.cost_cny += stats.cost_cny
79
- return subtotal
80
-
81
109
 
82
110
  def iter_all_sessions() -> list[tuple[str, Path]]:
83
111
  """Iterate over all sessions across all projects.
@@ -201,78 +229,52 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
201
229
  table.add_column("USD", justify="right")
202
230
  table.add_column("CNY", justify="right")
203
231
 
204
- # Sort dates
205
232
  sorted_dates = sorted(daily_stats.keys())
233
+ global_by_model: dict[ModelKey, ModelUsageStats] = {}
234
+
235
+ def add_stats_row(stats: ModelUsageStats, date_label: str = "", prefix: str = "", bold: bool = False) -> None:
236
+ """Add a single stats row to the table."""
237
+ usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
238
+ if prefix:
239
+ model_col = f"[bright_black dim]{prefix}[/bright_black dim]{stats.model_name}"
240
+ elif bold:
241
+ model_col = f"[bold]{stats.model_name}[/bold]"
242
+ else:
243
+ model_col = stats.model_name
206
244
 
207
- # Track global totals by (model, provider)
208
- global_by_model: dict[tuple[str, str], ModelUsageStats] = {}
245
+ def fmt(val: str) -> str:
246
+ return f"[bold]{val}[/bold]" if bold else val
209
247
 
210
- def sort_by_cost(stats: ModelUsageStats) -> tuple[float, float]:
211
- """Sort key: USD desc, then CNY desc."""
212
- return (-stats.cost_usd, -stats.cost_cny)
248
+ table.add_row(
249
+ date_label,
250
+ model_col,
251
+ fmt(format_tokens(stats.input_tokens)),
252
+ fmt(format_tokens(stats.output_tokens)),
253
+ fmt(format_tokens(stats.cached_tokens)),
254
+ fmt(format_tokens(stats.total_tokens)),
255
+ fmt(usd_str),
256
+ fmt(cny_str),
257
+ )
213
258
 
214
- def render_by_provider(
215
- models: dict[str, ModelUsageStats],
259
+ def render_grouped(
260
+ models: dict[ModelKey, ModelUsageStats],
216
261
  date_label: str = "",
217
262
  show_subtotal: bool = True,
218
263
  ) -> None:
219
264
  """Render models grouped by provider with tree structure."""
220
- # Group models by provider
221
- models_by_provider: dict[str, list[ModelUsageStats]] = {}
222
- provider_totals: dict[str, ModelUsageStats] = {}
223
- for stats in models.values():
224
- provider_key = stats.provider or "(unknown)"
225
- if provider_key not in models_by_provider:
226
- models_by_provider[provider_key] = []
227
- provider_totals[provider_key] = ModelUsageStats(model_name=provider_key, provider=provider_key)
228
- models_by_provider[provider_key].append(stats)
229
- provider_totals[provider_key].input_tokens += stats.input_tokens
230
- provider_totals[provider_key].output_tokens += stats.output_tokens
231
- provider_totals[provider_key].cached_tokens += stats.cached_tokens
232
- provider_totals[provider_key].cost_usd += stats.cost_usd
233
- provider_totals[provider_key].cost_cny += stats.cost_cny
234
-
235
- # Sort providers by cost, and models within each provider by cost
236
- sorted_providers = sorted(provider_totals.keys(), key=lambda p: sort_by_cost(provider_totals[p]))
237
- for provider_key in models_by_provider:
238
- models_by_provider[provider_key].sort(key=sort_by_cost)
265
+ models_by_provider, provider_totals = group_models_by_provider(models)
239
266
 
240
267
  first_row = True
241
- for provider_key in sorted_providers:
268
+ for provider_key, provider_models in models_by_provider.items():
242
269
  provider_stats = provider_totals[provider_key]
243
- provider_models = models_by_provider[provider_key]
244
-
245
- # Provider row (bold)
246
- usd_str, cny_str = format_cost_dual(provider_stats.cost_usd, provider_stats.cost_cny)
247
- table.add_row(
248
- date_label if first_row else "",
249
- f"[bold]{provider_key}[/bold]",
250
- f"[bold]{format_tokens(provider_stats.input_tokens)}[/bold]",
251
- f"[bold]{format_tokens(provider_stats.output_tokens)}[/bold]",
252
- f"[bold]{format_tokens(provider_stats.cached_tokens)}[/bold]",
253
- f"[bold]{format_tokens(provider_stats.total_tokens)}[/bold]",
254
- f"[bold]{usd_str}[/bold]",
255
- f"[bold]{cny_str}[/bold]",
256
- )
270
+ add_stats_row(provider_stats, date_label=date_label if first_row else "", bold=True)
257
271
  first_row = False
258
272
 
259
- # Model rows with tree prefix
260
273
  for i, stats in enumerate(provider_models):
261
274
  is_last = i == len(provider_models) - 1
262
275
  prefix = " └─ " if is_last else " ├─ "
263
- usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
264
- table.add_row(
265
- "",
266
- f"[bright_black dim]{prefix}[/bright_black dim]{stats.model_name}",
267
- format_tokens(stats.input_tokens),
268
- format_tokens(stats.output_tokens),
269
- format_tokens(stats.cached_tokens),
270
- format_tokens(stats.total_tokens),
271
- usd_str,
272
- cny_str,
273
- )
274
-
275
- # Add subtotal row
276
+ add_stats_row(stats, prefix=prefix)
277
+
276
278
  if show_subtotal:
277
279
  subtotal = ModelUsageStats(model_name="(subtotal)")
278
280
  for stats in models.values():
@@ -281,43 +283,29 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
281
283
  subtotal.cached_tokens += stats.cached_tokens
282
284
  subtotal.cost_usd += stats.cost_usd
283
285
  subtotal.cost_cny += stats.cost_cny
284
- usd_str, cny_str = format_cost_dual(subtotal.cost_usd, subtotal.cost_cny)
285
- table.add_row(
286
- "",
287
- "[bold](subtotal)[/bold]",
288
- f"[bold]{format_tokens(subtotal.input_tokens)}[/bold]",
289
- f"[bold]{format_tokens(subtotal.output_tokens)}[/bold]",
290
- f"[bold]{format_tokens(subtotal.cached_tokens)}[/bold]",
291
- f"[bold]{format_tokens(subtotal.total_tokens)}[/bold]",
292
- f"[bold]{usd_str}[/bold]",
293
- f"[bold]{cny_str}[/bold]",
294
- )
286
+ add_stats_row(subtotal, bold=True)
295
287
 
296
288
  for date_str in sorted_dates:
297
289
  day = daily_stats[date_str]
298
290
 
299
- # Accumulate to global totals by (model, provider)
300
- for model_name, stats in day.by_model.items():
301
- model_key = (model_name, stats.provider or "")
291
+ # Accumulate to global totals
292
+ for model_key, stats in day.by_model.items():
302
293
  if model_key not in global_by_model:
303
- global_by_model[model_key] = ModelUsageStats(model_name=model_name, provider=stats.provider)
294
+ global_by_model[model_key] = ModelUsageStats(model_name=stats.model_name, provider=stats.provider)
304
295
  global_by_model[model_key].input_tokens += stats.input_tokens
305
296
  global_by_model[model_key].output_tokens += stats.output_tokens
306
297
  global_by_model[model_key].cached_tokens += stats.cached_tokens
307
298
  global_by_model[model_key].cost_usd += stats.cost_usd
308
299
  global_by_model[model_key].cost_cny += stats.cost_cny
309
300
 
310
- # Render this day's data grouped by provider
311
- render_by_provider(day.by_model, date_label=format_date_display(date_str))
301
+ render_grouped(day.by_model, date_label=format_date_display(date_str))
312
302
 
313
- # Add separator between days
314
303
  if date_str != sorted_dates[-1]:
315
304
  table.add_section()
316
305
 
317
- # Add final section for totals
306
+ # Total section
318
307
  table.add_section()
319
308
 
320
- # Build date range label for Total
321
309
  if sorted_dates:
322
310
  first_date = format_date_display(sorted_dates[0])
323
311
  last_date = format_date_display(sorted_dates[-1])
@@ -328,64 +316,10 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
328
316
  else:
329
317
  total_label = "[bold]Total[/bold]"
330
318
 
331
- # Group models by provider
332
- models_by_provider: dict[str, list[ModelUsageStats]] = {}
333
- provider_totals: dict[str, ModelUsageStats] = {}
334
- for stats in global_by_model.values():
335
- provider_key = stats.provider or "(unknown)"
336
- if provider_key not in models_by_provider:
337
- models_by_provider[provider_key] = []
338
- provider_totals[provider_key] = ModelUsageStats(model_name=provider_key, provider=provider_key)
339
- models_by_provider[provider_key].append(stats)
340
- provider_totals[provider_key].input_tokens += stats.input_tokens
341
- provider_totals[provider_key].output_tokens += stats.output_tokens
342
- provider_totals[provider_key].cached_tokens += stats.cached_tokens
343
- provider_totals[provider_key].cost_usd += stats.cost_usd
344
- provider_totals[provider_key].cost_cny += stats.cost_cny
345
-
346
- # Sort providers by cost, and models within each provider by cost
347
- sorted_providers = sorted(provider_totals.keys(), key=lambda p: sort_by_cost(provider_totals[p]))
348
- for provider_key in models_by_provider:
349
- models_by_provider[provider_key].sort(key=sort_by_cost)
350
-
351
- # Add total label row
352
319
  table.add_row(total_label, "", "", "", "", "", "", "")
320
+ render_grouped(global_by_model, show_subtotal=False)
353
321
 
354
- # Render each provider with its models
355
- for provider_key in sorted_providers:
356
- provider_stats = provider_totals[provider_key]
357
- models = models_by_provider[provider_key]
358
-
359
- # Provider row (bold)
360
- usd_str, cny_str = format_cost_dual(provider_stats.cost_usd, provider_stats.cost_cny)
361
- table.add_row(
362
- "",
363
- f"[bold]{provider_key}[/bold]",
364
- f"[bold]{format_tokens(provider_stats.input_tokens)}[/bold]",
365
- f"[bold]{format_tokens(provider_stats.output_tokens)}[/bold]",
366
- f"[bold]{format_tokens(provider_stats.cached_tokens)}[/bold]",
367
- f"[bold]{format_tokens(provider_stats.total_tokens)}[/bold]",
368
- f"[bold]{usd_str}[/bold]",
369
- f"[bold]{cny_str}[/bold]",
370
- )
371
-
372
- # Model rows with tree prefix
373
- for i, stats in enumerate(models):
374
- is_last = i == len(models) - 1
375
- prefix = " └─ " if is_last else " ├─ "
376
- usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
377
- table.add_row(
378
- "",
379
- f"[bright_black dim]{prefix}[/bright_black dim]{stats.model_name}",
380
- format_tokens(stats.input_tokens),
381
- format_tokens(stats.output_tokens),
382
- format_tokens(stats.cached_tokens),
383
- format_tokens(stats.total_tokens),
384
- usd_str,
385
- cny_str,
386
- )
387
-
388
- # Add grand total row
322
+ # Grand total
389
323
  grand_total = ModelUsageStats(model_name="(total)")
390
324
  for stats in global_by_model.values():
391
325
  grand_total.input_tokens += stats.input_tokens
@@ -393,18 +327,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
393
327
  grand_total.cached_tokens += stats.cached_tokens
394
328
  grand_total.cost_usd += stats.cost_usd
395
329
  grand_total.cost_cny += stats.cost_cny
396
-
397
- usd_str, cny_str = format_cost_dual(grand_total.cost_usd, grand_total.cost_cny)
398
- table.add_row(
399
- "",
400
- "[bold](total)[/bold]",
401
- f"[bold]{format_tokens(grand_total.input_tokens)}[/bold]",
402
- f"[bold]{format_tokens(grand_total.output_tokens)}[/bold]",
403
- f"[bold]{format_tokens(grand_total.cached_tokens)}[/bold]",
404
- f"[bold]{format_tokens(grand_total.total_tokens)}[/bold]",
405
- f"[bold]{usd_str}[/bold]",
406
- f"[bold]{cny_str}[/bold]",
407
- )
330
+ add_stats_row(grand_total, bold=True)
408
331
 
409
332
  return table
410
333
 
@@ -121,6 +121,53 @@ def _get_claude_status_rows() -> list[tuple[Text, Text]]:
121
121
  return rows
122
122
 
123
123
 
124
+ def _get_antigravity_status_rows() -> list[tuple[Text, Text]]:
125
+ """Get Antigravity OAuth login status as (label, value) tuples for table display."""
126
+ from klaude_code.auth.antigravity.token_manager import AntigravityTokenManager
127
+
128
+ rows: list[tuple[Text, Text]] = []
129
+ token_manager = AntigravityTokenManager()
130
+ state = token_manager.get_state()
131
+
132
+ if state is None:
133
+ rows.append(
134
+ (
135
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
136
+ Text.assemble(
137
+ ("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
138
+ (" (run 'klaude login antigravity' to authenticate)", "dim"),
139
+ ),
140
+ )
141
+ )
142
+ elif state.is_expired():
143
+ rows.append(
144
+ (
145
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
146
+ Text.assemble(
147
+ ("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
148
+ (" (will refresh automatically on use; run 'klaude login antigravity' if refresh fails)", "dim"),
149
+ ),
150
+ )
151
+ )
152
+ else:
153
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
154
+ email_info = f", email: {state.email}" if state.email else ""
155
+ rows.append(
156
+ (
157
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
158
+ Text.assemble(
159
+ ("Logged in", ThemeKey.CONFIG_STATUS_OK),
160
+ (
161
+ f" (project: {state.project_id}{email_info}, expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})",
162
+ "dim",
163
+ ),
164
+ ),
165
+ )
166
+ )
167
+
168
+ return rows
169
+
170
+
124
171
  def mask_api_key(api_key: str | None) -> str:
125
172
  """Mask API key to show only first 6 and last 6 characters with *** in between"""
126
173
  if not api_key:
@@ -234,6 +281,9 @@ def _build_provider_info_panel(provider: ProviderConfig, available: bool) -> Quo
234
281
  if provider.protocol == LLMClientProtocol.CLAUDE_OAUTH:
235
282
  for label, value in _get_claude_status_rows():
236
283
  info_table.add_row(label, value)
284
+ if provider.protocol == LLMClientProtocol.ANTIGRAVITY:
285
+ for label, value in _get_antigravity_status_rows():
286
+ info_table.add_row(label, value)
237
287
 
238
288
  return Quote(
239
289
  Group(title, info_table),
klaude_code/cli/main.py CHANGED
@@ -1,7 +1,10 @@
1
1
  import asyncio
2
2
  import sys
3
+ from collections.abc import Sequence
4
+ from typing import Any
3
5
 
4
6
  import typer
7
+ from typer.core import TyperGroup
5
8
 
6
9
  from klaude_code.cli.auth_cmd import register_auth_commands
7
10
  from klaude_code.cli.config_cmd import register_config_commands
@@ -40,7 +43,88 @@ def _build_env_help() -> str:
40
43
 
41
44
  ENV_HELP = _build_env_help()
42
45
 
46
+
47
+ def _looks_like_flag(token: str) -> bool:
48
+ return token.startswith("-") and token != "-"
49
+
50
+
51
+ def _preprocess_cli_args(args: list[str]) -> list[str]:
52
+ """Rewrite CLI args to support optional values for selected options.
53
+
54
+ Supported rewrites:
55
+ - --model / -m with no value -> --model-select
56
+ - --resume / -r with value -> --resume-by-id <value>
57
+ """
58
+
59
+ rewritten: list[str] = []
60
+ i = 0
61
+ while i < len(args):
62
+ token = args[i]
63
+
64
+ if token in {"--model", "-m"}:
65
+ next_token = args[i + 1] if i + 1 < len(args) else None
66
+ if next_token is None or next_token == "--" or _looks_like_flag(next_token):
67
+ rewritten.append("--model-select")
68
+ i += 1
69
+ continue
70
+ rewritten.append(token)
71
+ i += 1
72
+ continue
73
+
74
+ if token.startswith("--model="):
75
+ value = token.split("=", 1)[1]
76
+ if value == "":
77
+ rewritten.append("--model-select")
78
+ else:
79
+ rewritten.append(token)
80
+ i += 1
81
+ continue
82
+
83
+ if token in {"--resume", "-r"}:
84
+ next_token = args[i + 1] if i + 1 < len(args) else None
85
+ if next_token is not None and next_token != "--" and not _looks_like_flag(next_token):
86
+ rewritten.extend(["--resume-by-id", next_token])
87
+ i += 2
88
+ continue
89
+ rewritten.append(token)
90
+ i += 1
91
+ continue
92
+
93
+ if token.startswith("--resume="):
94
+ value = token.split("=", 1)[1]
95
+ rewritten.extend(["--resume-by-id", value])
96
+ i += 1
97
+ continue
98
+
99
+ rewritten.append(token)
100
+ i += 1
101
+
102
+ return rewritten
103
+
104
+
105
+ class _PreprocessingTyperGroup(TyperGroup):
106
+ def main(
107
+ self,
108
+ args: Sequence[str] | None = None,
109
+ prog_name: str | None = None,
110
+ complete_var: str | None = None,
111
+ standalone_mode: bool = True,
112
+ windows_expand_args: bool = True,
113
+ **extra: Any,
114
+ ) -> Any:
115
+ click_args = _preprocess_cli_args(list(args) if args is not None else sys.argv[1:])
116
+ return super().main(
117
+ args=click_args,
118
+ prog_name=prog_name,
119
+ complete_var=complete_var,
120
+ standalone_mode=standalone_mode,
121
+ windows_expand_args=windows_expand_args,
122
+ **extra,
123
+ )
124
+
125
+
43
126
  app = typer.Typer(
127
+ cls=_PreprocessingTyperGroup,
44
128
  add_completion=False,
45
129
  pretty_exceptions_enable=False,
46
130
  no_args_is_help=False,
@@ -69,21 +153,27 @@ def main_callback(
69
153
  None,
70
154
  "--model",
71
155
  "-m",
72
- help="Select model by name",
156
+ help="Select model by name; use --model with no value to choose interactively",
73
157
  rich_help_panel="LLM",
74
158
  ),
75
159
  continue_: bool = typer.Option(False, "--continue", "-c", help="Resume latest session"),
76
- resume: bool = typer.Option(False, "--resume", "-r", help="Pick a session to resume"),
160
+ resume: bool = typer.Option(
161
+ False,
162
+ "--resume",
163
+ "-r",
164
+ help="Resume a session; use --resume <id> to resume directly, or --resume to pick interactively",
165
+ ),
77
166
  resume_by_id: str | None = typer.Option(
78
167
  None,
79
168
  "--resume-by-id",
80
169
  help="Resume session by ID",
170
+ hidden=True,
81
171
  ),
82
172
  select_model: bool = typer.Option(
83
173
  False,
84
- "--select-model",
85
- "-s",
86
- help="Choose model interactively",
174
+ "--model-select",
175
+ help="Choose model interactively (same as --model with no value)",
176
+ hidden=True,
87
177
  rich_help_panel="LLM",
88
178
  ),
89
179
  debug: bool = typer.Option(
@@ -107,7 +197,7 @@ def main_callback(
107
197
  banana: bool = typer.Option(
108
198
  False,
109
199
  "--banana",
110
- help="Image generation mode",
200
+ help="Image generation mode (alias for --model banana)",
111
201
  rich_help_panel="LLM",
112
202
  ),
113
203
  version: bool = typer.Option(
@@ -130,11 +220,11 @@ def main_callback(
130
220
 
131
221
  resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
132
222
  if resume_by_id_value == "":
133
- log(("Error: --resume-by-id cannot be empty", "red"))
223
+ log(("Error: --resume <id> cannot be empty", "red"))
134
224
  raise typer.Exit(2)
135
225
 
136
226
  if resume_by_id_value is not None and (resume or continue_):
137
- log(("Error: --resume-by-id cannot be combined with --resume/--continue", "red"))
227
+ log(("Error: --resume <id> cannot be combined with --continue or interactive --resume", "red"))
138
228
  raise typer.Exit(2)
139
229
 
140
230
  if resume_by_id_value is not None and not Session.exists(resume_by_id_value):
@@ -250,7 +340,7 @@ def main_callback(
250
340
  )
251
341
 
252
342
  if log_path:
253
- log(f"Debug log: {log_path}", style="dim")
343
+ log(f"Debug log: {log_path}", style="red")
254
344
 
255
345
  asyncio.run(
256
346
  run_interactive(