klaude-code 1.2.26__py3-none-any.whl → 1.2.27__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 (36) hide show
  1. klaude_code/cli/config_cmd.py +1 -5
  2. klaude_code/cli/list_model.py +170 -129
  3. klaude_code/cli/main.py +37 -5
  4. klaude_code/cli/runtime.py +4 -6
  5. klaude_code/cli/self_update.py +2 -1
  6. klaude_code/cli/session_cmd.py +1 -1
  7. klaude_code/config/__init__.py +3 -1
  8. klaude_code/config/assets/__init__.py +1 -0
  9. klaude_code/config/assets/builtin_config.yaml +233 -0
  10. klaude_code/config/builtin_config.py +37 -0
  11. klaude_code/config/config.py +332 -112
  12. klaude_code/config/select_model.py +45 -8
  13. klaude_code/core/executor.py +4 -2
  14. klaude_code/core/manager/llm_clients_builder.py +4 -1
  15. klaude_code/core/tool/file/edit_tool.py +4 -4
  16. klaude_code/core/tool/file/write_tool.py +4 -4
  17. klaude_code/core/tool/shell/bash_tool.py +2 -2
  18. klaude_code/llm/openai_compatible/stream.py +2 -1
  19. klaude_code/session/export.py +1 -1
  20. klaude_code/session/selector.py +2 -2
  21. klaude_code/session/session.py +4 -4
  22. klaude_code/ui/modes/repl/completers.py +4 -4
  23. klaude_code/ui/modes/repl/event_handler.py +1 -1
  24. klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -4
  25. klaude_code/ui/modes/repl/key_bindings.py +4 -4
  26. klaude_code/ui/renderers/diffs.py +1 -1
  27. klaude_code/ui/renderers/metadata.py +2 -2
  28. klaude_code/ui/renderers/tools.py +1 -1
  29. klaude_code/ui/rich/markdown.py +1 -1
  30. klaude_code/ui/rich/theme.py +1 -1
  31. klaude_code/ui/terminal/color.py +1 -1
  32. klaude_code/ui/terminal/control.py +4 -4
  33. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/METADATA +121 -127
  34. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/RECORD +36 -33
  35. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/WHEEL +0 -0
  36. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/entry_points.txt +0 -0
@@ -16,8 +16,6 @@ def list_models() -> None:
16
16
  from klaude_code.ui.terminal.color import is_light_terminal_background
17
17
 
18
18
  config = load_config()
19
- if config is None:
20
- raise typer.Exit(1)
21
19
 
22
20
  # Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
23
21
  if config.theme is None:
@@ -60,9 +58,7 @@ def edit_config() -> None:
60
58
  editor = "xdg-open"
61
59
 
62
60
  # Ensure config file exists
63
- config = load_config()
64
- if config is None:
65
- raise typer.Exit(1)
61
+ load_config()
66
62
 
67
63
  try:
68
64
  if editor == "open -a TextEdit":
@@ -6,44 +6,47 @@ from rich.table import Table
6
6
  from rich.text import Text
7
7
 
8
8
  from klaude_code.config import Config
9
+ from klaude_code.config.config import ModelConfig, ProviderConfig
10
+ from klaude_code.protocol.llm_param import LLMClientProtocol
9
11
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
10
12
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
11
13
 
12
14
 
13
- def _display_codex_status(console: Console) -> None:
14
- """Display Codex OAuth login status."""
15
+ def _get_codex_status_elements() -> list[Text]:
16
+ """Get Codex OAuth login status as Text elements for panel display."""
15
17
  from klaude_code.auth.codex.token_manager import CodexTokenManager
16
18
 
19
+ elements: list[Text] = []
17
20
  token_manager = CodexTokenManager()
18
21
  state = token_manager.get_state()
19
22
 
20
23
  if state is None:
21
- console.print(
24
+ elements.append(
22
25
  Text.assemble(
23
- ("Codex Status: ", "bold"),
26
+ ("Status: ", "bold"),
24
27
  ("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
25
28
  (" (run 'klaude login codex' to authenticate)", "dim"),
26
29
  )
27
30
  )
28
31
  elif state.is_expired():
29
- console.print(
32
+ elements.append(
30
33
  Text.assemble(
31
- ("Codex Status: ", "bold"),
34
+ ("Status: ", "bold"),
32
35
  ("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
33
36
  (" (run 'klaude login codex' to re-authenticate)", "dim"),
34
37
  )
35
38
  )
36
39
  else:
37
40
  expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
38
- console.print(
41
+ elements.append(
39
42
  Text.assemble(
40
- ("Codex Status: ", "bold"),
43
+ ("Status: ", "bold"),
41
44
  ("Logged in", ThemeKey.CONFIG_STATUS_OK),
42
45
  (f" (account: {state.account_id[:8]}..., expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})", "dim"),
43
46
  )
44
47
  )
45
48
 
46
- console.print(
49
+ elements.append(
47
50
  Text.assemble(
48
51
  ("Visit ", "dim"),
49
52
  (
@@ -53,6 +56,7 @@ def _display_codex_status(console: Console) -> None:
53
56
  (" for up-to-date information on rate limits and credits", "dim"),
54
57
  )
55
58
  )
59
+ return elements
56
60
 
57
61
 
58
62
  def mask_api_key(api_key: str | None) -> str:
@@ -66,136 +70,179 @@ def mask_api_key(api_key: str | None) -> str:
66
70
  return f"{api_key[:6]} … {api_key[-6:]}"
67
71
 
68
72
 
69
- def display_models_and_providers(config: Config):
70
- """Display models and providers configuration using rich formatting"""
71
- themes = get_theme(config.theme)
72
- console = Console(theme=themes.app_theme)
73
+ def format_api_key_display(provider: ProviderConfig) -> Text:
74
+ """Format API key display with warning if env var is not set."""
75
+ env_var = provider.get_api_key_env_var()
76
+ resolved_key = provider.get_resolved_api_key()
73
77
 
74
- # Display providers section
75
- providers_table = Table.grid(padding=(0, 1), expand=True)
76
- providers_table.add_column(width=2, no_wrap=True) # Status
77
- providers_table.add_column(overflow="fold") # Name
78
- providers_table.add_column(overflow="fold") # Protocol
79
- providers_table.add_column(overflow="fold") # Base URL
80
- providers_table.add_column(overflow="fold") # API Key
81
-
82
- # Add header
83
- providers_table.add_row(
84
- Text("", style="bold"),
85
- Text("Name", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
86
- Text("Protocol", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
87
- Text("Base URL", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
88
- Text("API Key", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
89
- )
78
+ if env_var:
79
+ # Using ${ENV_VAR} syntax
80
+ if resolved_key:
81
+ return Text.assemble(
82
+ (f"${{{env_var}}} = ", "dim"),
83
+ (mask_api_key(resolved_key), ""),
84
+ )
85
+ else:
86
+ return Text.assemble(
87
+ (f"${{{env_var}}} ", ""),
88
+ ("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
89
+ )
90
+ elif provider.api_key:
91
+ # Plain API key
92
+ return Text(mask_api_key(provider.api_key))
93
+ else:
94
+ return Text("N/A")
90
95
 
91
- # Add providers
92
- for provider in config.provider_list:
93
- status = Text("✔", style=f"bold {ThemeKey.CONFIG_STATUS_OK}")
94
- name = Text(provider.provider_name, style=ThemeKey.CONFIG_ITEM_NAME)
95
- protocol = Text(str(provider.protocol.value), style="")
96
- base_url = Text(provider.base_url or "N/A", style="")
97
- api_key = Text(mask_api_key(provider.api_key), style="")
98
-
99
- providers_table.add_row(status, name, protocol, base_url, api_key)
100
-
101
- # Display models section
102
- models_table = Table.grid(padding=(0, 1), expand=True)
103
- models_table.add_column(width=2, no_wrap=True) # Status
104
- models_table.add_column(overflow="fold", ratio=1) # Name
105
- models_table.add_column(overflow="fold", ratio=2) # Model
106
- models_table.add_column(overflow="fold", ratio=2) # Provider
107
- models_table.add_column(overflow="fold", ratio=3) # Params
108
-
109
- # Add header
110
- models_table.add_row(
111
- Text("", style="bold"),
112
- Text("Name", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
113
- Text("Model", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
114
- Text("Provider", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
115
- Text("Params", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
116
- )
117
96
 
118
- # Add models
119
- for model in config.model_list:
120
- status = Text("✔", style=f"bold {ThemeKey.CONFIG_STATUS_OK}")
121
- if model.model_name == config.main_model:
122
- status = Text("★", style=f"bold {ThemeKey.CONFIG_STATUS_PRIMARY}") # Mark main model
123
-
124
- name = Text(
125
- model.model_name,
126
- style=ThemeKey.CONFIG_STATUS_PRIMARY
127
- if model.model_name == config.main_model
128
- else ThemeKey.CONFIG_ITEM_NAME,
129
- )
130
- model_name = Text(model.model_params.model or "N/A", style="")
131
- provider = Text(model.provider, style="")
132
- params: list[Text] = []
133
- if model.model_params.thinking:
134
- if model.model_params.thinking.reasoning_effort is not None:
135
- params.append(
136
- Text.assemble(
137
- ("reason-effort", ThemeKey.CONFIG_PARAM_LABEL),
138
- ": ",
139
- model.model_params.thinking.reasoning_effort,
140
- )
141
- )
142
- if model.model_params.thinking.reasoning_summary is not None:
143
- params.append(
144
- Text.assemble(
145
- ("reason-summary", ThemeKey.CONFIG_PARAM_LABEL),
146
- ": ",
147
- model.model_params.thinking.reasoning_summary,
148
- )
97
+ def _get_model_params_display(model: ModelConfig) -> list[Text]:
98
+ """Get display elements for model parameters."""
99
+ params: list[Text] = []
100
+ if model.model_params.thinking:
101
+ if model.model_params.thinking.reasoning_effort is not None:
102
+ params.append(
103
+ Text.assemble(
104
+ ("reason-effort", ThemeKey.CONFIG_PARAM_LABEL),
105
+ ": ",
106
+ model.model_params.thinking.reasoning_effort,
149
107
  )
150
- if model.model_params.thinking.budget_tokens is not None:
151
- params.append(
152
- Text.assemble(
153
- ("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
154
- ": ",
155
- str(model.model_params.thinking.budget_tokens),
156
- )
108
+ )
109
+ if model.model_params.thinking.reasoning_summary is not None:
110
+ params.append(
111
+ Text.assemble(
112
+ ("reason-summary", ThemeKey.CONFIG_PARAM_LABEL),
113
+ ": ",
114
+ model.model_params.thinking.reasoning_summary,
157
115
  )
158
- if model.model_params.provider_routing:
116
+ )
117
+ if model.model_params.thinking.budget_tokens is not None:
159
118
  params.append(
160
119
  Text.assemble(
161
- ("provider-routing", ThemeKey.CONFIG_PARAM_LABEL),
120
+ ("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
162
121
  ": ",
163
- model.model_params.provider_routing.model_dump_json(exclude_none=True),
122
+ str(model.model_params.thinking.budget_tokens),
164
123
  )
165
124
  )
166
- if len(params) == 0:
167
- params.append(Text("N/A", style=ThemeKey.CONFIG_PARAM_LABEL))
168
- models_table.add_row(status, name, model_name, provider, Group(*params))
169
-
170
- # Create panels and display
171
- providers_panel = Panel(
172
- providers_table,
173
- title=Text("Providers Configuration", style="white bold"),
174
- border_style=ThemeKey.CONFIG_PANEL_BORDER,
175
- padding=(0, 1),
176
- title_align="left",
177
- )
125
+ if model.model_params.provider_routing:
126
+ params.append(
127
+ Text.assemble(
128
+ ("provider-routing", ThemeKey.CONFIG_PARAM_LABEL),
129
+ ": ",
130
+ model.model_params.provider_routing.model_dump_json(exclude_none=True),
131
+ )
132
+ )
133
+ if len(params) == 0:
134
+ params.append(Text("N/A", style=ThemeKey.CONFIG_PARAM_LABEL))
135
+ return params
178
136
 
179
- models_panel = Panel(
180
- models_table,
181
- title=Text("Models Configuration", style="white bold"),
182
- border_style=ThemeKey.CONFIG_PANEL_BORDER,
183
- padding=(0, 1),
184
- title_align="left",
185
- )
186
137
 
187
- console.print(providers_panel)
188
- console.print()
189
- console.print(models_panel)
138
+ def display_models_and_providers(config: Config):
139
+ """Display models and providers configuration using rich formatting"""
140
+ themes = get_theme(config.theme)
141
+ console = Console(theme=themes.app_theme)
142
+
143
+ # Display each provider as a separate panel
144
+ for provider in config.provider_list:
145
+ # Provider info section
146
+ provider_info = Table.grid(padding=(0, 1))
147
+ provider_info.add_column(width=12)
148
+ provider_info.add_column()
149
+
150
+ provider_info.add_row(
151
+ Text("Protocol:", style=ThemeKey.CONFIG_PARAM_LABEL),
152
+ Text(provider.protocol.value),
153
+ )
154
+ if provider.base_url:
155
+ provider_info.add_row(
156
+ Text("Base URL:", style=ThemeKey.CONFIG_PARAM_LABEL),
157
+ Text(provider.base_url or "N/A"),
158
+ )
159
+ if provider.api_key:
160
+ provider_info.add_row(
161
+ Text("API Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
162
+ format_api_key_display(provider),
163
+ )
164
+
165
+ # Check if provider has valid API key
166
+ provider_available = not provider.is_api_key_missing()
167
+
168
+ # Models table for this provider
169
+ models_table = Table.grid(padding=(0, 1), expand=True)
170
+ models_table.add_column(width=2, no_wrap=True) # Status
171
+ models_table.add_column(overflow="fold", ratio=1) # Name
172
+ models_table.add_column(overflow="fold", ratio=2) # Model
173
+ models_table.add_column(overflow="fold", ratio=3) # Params
174
+
175
+ # Add header
176
+ models_table.add_row(
177
+ Text("", style="bold"),
178
+ Text("Name", style=ThemeKey.CONFIG_TABLE_HEADER),
179
+ Text("Model", style=ThemeKey.CONFIG_TABLE_HEADER),
180
+ Text("Params", style=ThemeKey.CONFIG_TABLE_HEADER),
181
+ )
182
+
183
+ # Add models for this provider
184
+ for model in provider.model_list:
185
+ if not provider_available:
186
+ # Provider API key not set - show as unavailable
187
+ status = Text("-", style="dim")
188
+ name = Text(model.model_name, style="dim")
189
+ model_id = Text(model.model_params.model or "N/A", style="dim")
190
+ params = [Text("(unavailable)", style="dim")]
191
+ elif model.model_name == config.main_model:
192
+ status = Text("★", style=ThemeKey.CONFIG_STATUS_PRIMARY)
193
+ name = Text(model.model_name, style=ThemeKey.CONFIG_STATUS_PRIMARY)
194
+ model_id = Text(model.model_params.model or "N/A", style="")
195
+ params = _get_model_params_display(model)
196
+ else:
197
+ status = Text("✔", style=ThemeKey.CONFIG_STATUS_OK)
198
+ name = Text(model.model_name, style=ThemeKey.CONFIG_ITEM_NAME)
199
+ model_id = Text(model.model_params.model or "N/A", style="")
200
+ params = _get_model_params_display(model)
201
+
202
+ models_table.add_row(status, name, model_id, Group(*params))
203
+
204
+ # Create panel content with provider info and models
205
+ panel_elements = [
206
+ provider_info,
207
+ Text(""), # Spacer
208
+ Text("Models:", style=ThemeKey.CONFIG_TABLE_HEADER),
209
+ models_table,
210
+ ]
211
+
212
+ # Add Codex status if this is a codex provider
213
+ if provider.protocol == LLMClientProtocol.CODEX:
214
+ panel_elements.append(Text("")) # Spacer
215
+ panel_elements.extend(_get_codex_status_elements())
216
+
217
+ panel_content = Group(*panel_elements)
218
+
219
+ panel = Panel(
220
+ panel_content,
221
+ title=Text(f"Provider: {provider.provider_name}", style="white bold"),
222
+ border_style=ThemeKey.CONFIG_PANEL_BORDER,
223
+ padding=(0, 1),
224
+ title_align="left",
225
+ )
226
+
227
+ console.print(panel)
228
+ console.print()
190
229
 
191
230
  # Display main model info
192
231
  console.print()
193
- console.print(
194
- Text.assemble(
195
- ("Default Model: ", "bold"),
196
- (config.main_model, ThemeKey.CONFIG_STATUS_PRIMARY),
232
+ if config.main_model:
233
+ console.print(
234
+ Text.assemble(
235
+ ("Default Model: ", "bold"),
236
+ (config.main_model, ThemeKey.CONFIG_STATUS_PRIMARY),
237
+ )
238
+ )
239
+ else:
240
+ console.print(
241
+ Text.assemble(
242
+ ("Default Model: ", "bold"),
243
+ ("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
244
+ )
197
245
  )
198
- )
199
246
 
200
247
  for profile in iter_sub_agent_profiles():
201
248
  sub_model_name = config.sub_agent_models.get(profile.name)
@@ -207,9 +254,3 @@ def display_models_and_providers(config: Config):
207
254
  (sub_model_name, ThemeKey.CONFIG_STATUS_PRIMARY),
208
255
  )
209
256
  )
210
-
211
- # Display Codex login status if any codex provider is configured
212
- has_codex_provider = any(p.protocol.value == "codex" for p in config.provider_list)
213
- if has_codex_provider:
214
- console.print()
215
- _display_codex_status(console)
klaude_code/cli/main.py CHANGED
@@ -143,13 +143,28 @@ def exec_command(
143
143
  raise typer.Exit(1)
144
144
 
145
145
  from klaude_code.cli.runtime import AppInitConfig, run_exec
146
+ from klaude_code.config import load_config
146
147
  from klaude_code.config.select_model import select_model_from_config
147
148
 
148
149
  chosen_model = model
149
150
  if model or select_model:
150
151
  chosen_model = select_model_from_config(preferred=model)
151
152
  if chosen_model is None:
152
- return
153
+ raise typer.Exit(1)
154
+ else:
155
+ # Check if main_model is configured; if not, trigger interactive selection
156
+ config = load_config()
157
+ if config.main_model is None:
158
+ chosen_model = select_model_from_config()
159
+ if chosen_model is None:
160
+ raise typer.Exit(1)
161
+ # Save the selection as default
162
+ config.main_model = chosen_model
163
+ from klaude_code.config.config import config_path
164
+ from klaude_code.trace import log
165
+
166
+ asyncio.run(config.save())
167
+ log(f"Saved main_model={chosen_model} to {config_path}", style="cyan")
153
168
 
154
169
  debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
155
170
 
@@ -275,8 +290,8 @@ def main_callback(
275
290
  session_meta = Session.load_meta(session_id)
276
291
  cfg = load_config()
277
292
 
278
- if cfg is not None and session_meta.model_config_name:
279
- if any(m.model_name == session_meta.model_config_name for m in cfg.model_list):
293
+ if session_meta.model_config_name:
294
+ if any(m.model_name == session_meta.model_config_name for m in cfg.iter_model_entries()):
280
295
  chosen_model = session_meta.model_config_name
281
296
  else:
282
297
  log(
@@ -286,17 +301,34 @@ def main_callback(
286
301
  )
287
302
  )
288
303
 
289
- if cfg is not None and chosen_model is None and session_meta.model_name:
304
+ if chosen_model is None and session_meta.model_name:
290
305
  raw_model = session_meta.model_name.strip()
291
306
  if raw_model:
292
307
  matches = [
293
308
  m.model_name
294
- for m in cfg.model_list
309
+ for m in cfg.iter_model_entries()
295
310
  if (m.model_params.model or "").strip().lower() == raw_model.lower()
296
311
  ]
297
312
  if len(matches) == 1:
298
313
  chosen_model = matches[0]
299
314
 
315
+ # If still no model, check main_model; if not configured, trigger interactive selection
316
+ if chosen_model is None:
317
+ from klaude_code.config import load_config
318
+
319
+ cfg = load_config()
320
+ if cfg.main_model is None:
321
+ chosen_model = select_model_from_config()
322
+ if chosen_model is None:
323
+ raise typer.Exit(1)
324
+ # Save the selection as default
325
+ cfg.main_model = chosen_model
326
+ from klaude_code.config.config import config_path
327
+ from klaude_code.trace import log
328
+
329
+ asyncio.run(cfg.save())
330
+ log(f"Saved main_model={chosen_model} to {config_path}", style="dim")
331
+
300
332
  debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
301
333
 
302
334
  init_config = AppInitConfig(
@@ -62,8 +62,6 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
62
62
  set_debug_logging(init_config.debug, filters=init_config.debug_filters)
63
63
 
64
64
  config = load_config()
65
- if config is None:
66
- raise typer.Exit(1)
67
65
 
68
66
  # Initialize LLM clients
69
67
  try:
@@ -160,7 +158,7 @@ async def initialize_session(
160
158
  def _backfill_session_model_config(
161
159
  agent: Agent | None,
162
160
  model_override: str | None,
163
- default_model: str,
161
+ default_model: str | None,
164
162
  is_new_session: bool,
165
163
  ) -> None:
166
164
  """Backfill model_config_name and model_thinking on newly created sessions."""
@@ -169,7 +167,7 @@ def _backfill_session_model_config(
169
167
 
170
168
  if model_override is not None:
171
169
  agent.session.model_config_name = model_override
172
- elif is_new_session:
170
+ elif is_new_session and default_model is not None:
173
171
  agent.session.model_config_name = default_model
174
172
  else:
175
173
  return
@@ -305,8 +303,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
305
303
  printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
306
304
  else:
307
305
  print(MSG, file=sys.stderr)
308
- except Exception:
309
- # Fallback if themed print is unavailable
306
+ except (AttributeError, TypeError, RuntimeError):
307
+ # Fallback if themed print is unavailable (e.g., display not ready or Rich internal error)
310
308
  print(MSG, file=sys.stderr)
311
309
 
312
310
  def _hide_progress() -> None:
@@ -174,7 +174,8 @@ def _print_version() -> None:
174
174
  ver = pkg_version(PACKAGE_NAME)
175
175
  except PackageNotFoundError:
176
176
  ver = "unknown"
177
- except Exception:
177
+ except (ValueError, TypeError):
178
+ # Catch invalid package name format or type errors
178
179
  ver = "unknown"
179
180
  print(f"{PACKAGE_NAME} {ver}")
180
181
 
@@ -13,7 +13,7 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
13
13
  def _fmt(ts: float) -> str:
14
14
  try:
15
15
  return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
16
- except Exception:
16
+ except (OSError, OverflowError, ValueError):
17
17
  return str(ts)
18
18
 
19
19
  log(f"Sessions to delete ({len(sessions)}):")
@@ -1,7 +1,9 @@
1
- from .config import Config, config_path, load_config
1
+ from .config import Config, UserConfig, config_path, load_config, print_no_available_models_hint
2
2
 
3
3
  __all__ = [
4
4
  "Config",
5
+ "UserConfig",
5
6
  "config_path",
6
7
  "load_config",
8
+ "print_no_available_models_hint",
7
9
  ]
@@ -0,0 +1 @@
1
+ # Asset files for config module