vmcode-cli 1.0.0

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 (56) hide show
  1. package/INSTALLATION_METHODS.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +199 -0
  4. package/bin/npm-wrapper.js +171 -0
  5. package/bin/rg +0 -0
  6. package/bin/rg.exe +0 -0
  7. package/config.yaml.example +159 -0
  8. package/package.json +42 -0
  9. package/requirements.txt +7 -0
  10. package/scripts/install.js +132 -0
  11. package/setup.bat +114 -0
  12. package/setup.sh +135 -0
  13. package/src/__init__.py +4 -0
  14. package/src/core/__init__.py +1 -0
  15. package/src/core/agentic.py +2342 -0
  16. package/src/core/chat_manager.py +1201 -0
  17. package/src/core/config_manager.py +269 -0
  18. package/src/core/init.py +161 -0
  19. package/src/core/sub_agent.py +174 -0
  20. package/src/exceptions.py +75 -0
  21. package/src/llm/__init__.py +1 -0
  22. package/src/llm/client.py +149 -0
  23. package/src/llm/config.py +445 -0
  24. package/src/llm/prompts.py +569 -0
  25. package/src/llm/providers.py +402 -0
  26. package/src/llm/token_tracker.py +220 -0
  27. package/src/ui/__init__.py +1 -0
  28. package/src/ui/banner.py +103 -0
  29. package/src/ui/commands.py +489 -0
  30. package/src/ui/displays.py +167 -0
  31. package/src/ui/main.py +351 -0
  32. package/src/ui/prompt_utils.py +162 -0
  33. package/src/utils/__init__.py +1 -0
  34. package/src/utils/editor.py +158 -0
  35. package/src/utils/gitignore_filter.py +149 -0
  36. package/src/utils/logger.py +254 -0
  37. package/src/utils/markdown.py +32 -0
  38. package/src/utils/settings.py +94 -0
  39. package/src/utils/tools/__init__.py +55 -0
  40. package/src/utils/tools/command_executor.py +217 -0
  41. package/src/utils/tools/create_file.py +143 -0
  42. package/src/utils/tools/definitions.py +193 -0
  43. package/src/utils/tools/directory.py +374 -0
  44. package/src/utils/tools/file_editor.py +345 -0
  45. package/src/utils/tools/file_helpers.py +109 -0
  46. package/src/utils/tools/file_reader.py +331 -0
  47. package/src/utils/tools/formatters.py +458 -0
  48. package/src/utils/tools/parallel_executor.py +195 -0
  49. package/src/utils/validation.py +117 -0
  50. package/src/utils/web_search.py +71 -0
  51. package/vmcode-proxy/.env.example +5 -0
  52. package/vmcode-proxy/README.md +235 -0
  53. package/vmcode-proxy/package-lock.json +947 -0
  54. package/vmcode-proxy/package.json +20 -0
  55. package/vmcode-proxy/server.js +248 -0
  56. package/vmcode-proxy/server.js.bak +157 -0
@@ -0,0 +1,103 @@
1
+ """Startup banner display - separated from main.py to avoid circular imports."""
2
+
3
+ import os
4
+ from rich.console import Console
5
+ from rich.theme import Theme
6
+ from rich.panel import Panel
7
+ from rich.text import Text
8
+ from rich.table import Table
9
+ from llm import config
10
+
11
+ console = Console(theme=Theme({
12
+ "markdown.hr": "grey50",
13
+ "markdown.heading": "default",
14
+ "markdown.h1": "default",
15
+ "markdown.h2": "default",
16
+ "markdown.h3": "default",
17
+ "markdown.h4": "default",
18
+ "markdown.h5": "default",
19
+ "markdown.h6": "default",
20
+ }))
21
+
22
+
23
+ def format_directory_path(path: str) -> str:
24
+ """Format directory path to show first and last parts with ellipsis.
25
+
26
+ Args:
27
+ path: Full directory path.
28
+
29
+ Returns:
30
+ Shortened path like 'c:/.../vmCode' or full path if short enough.
31
+ """
32
+ parts = path.split(os.sep)
33
+ if len(parts) > 2:
34
+ return f"{parts[0]}.../{parts[-1]}"
35
+ return path
36
+
37
+
38
+ def display_startup_banner(approve_mode: str, interaction_mode: str = "edit"):
39
+ """Ultra-minimalist startup screen for vmCode.
40
+
41
+ Args:
42
+ approve_mode: Current approval mode setting.
43
+ interaction_mode: Current interaction mode ('plan', 'edit', or 'learn').
44
+ """
45
+ console.clear()
46
+
47
+ # Get model name based on provider
48
+ provider_config = config.get_provider_config(config.LLM_PROVIDER)
49
+ if config.LLM_PROVIDER == "local":
50
+ model_path = provider_config.get("model") or ""
51
+ model_name = os.path.basename(model_path) if model_path else "None"
52
+ else:
53
+ model_name = provider_config.get("model") or "None"
54
+
55
+ # Get and format current directory
56
+ current_dir = os.getcwd()
57
+ formatted_dir = format_directory_path(current_dir)
58
+
59
+ # Create grid for 2-column layout
60
+ grid = Table.grid(expand=True)
61
+ grid.add_column(justify="left", ratio=1)
62
+ grid.add_column(justify="right", ratio=1)
63
+
64
+ # Add content
65
+ grid.add_row(
66
+ Text("vmCode", style="bold white"),
67
+ Text("v1.0.0", style="dim white")
68
+ )
69
+
70
+ model_info = Text.assemble(
71
+ (f"{config.LLM_PROVIDER.upper()} ", "bold cyan"),
72
+ (f"{model_name}", "grey70")
73
+ )
74
+
75
+ grid.add_row(
76
+ model_info,
77
+ Text(formatted_dir, style="dim grey50")
78
+ )
79
+
80
+ # Display in panel
81
+ console.print(Panel(
82
+ grid,
83
+ border_style="grey23",
84
+ padding=(0, 2)
85
+ ))
86
+
87
+ # Show vmcode_free notice if using free tier
88
+ if config.LLM_PROVIDER == "vmcode_free":
89
+ console.print(Panel(
90
+ Text.assemble(
91
+ ("✨ ", "bright_yellow"),
92
+ ("Using vmCode Free Model", "bold bright_yellow"),
93
+ ("\n\n", ""),
94
+ ("No API key required • Conversations routed through vmCode proxy\n", "dim"),
95
+ ("Switch providers with ", "dim"),
96
+ ("/provider", "cyan"),
97
+ (" command", "dim")
98
+ ),
99
+ border_style="bright_yellow",
100
+ padding=(1, 2)
101
+ ))
102
+ console.print()
103
+
@@ -0,0 +1,489 @@
1
+ """Command routing and help display."""
2
+
3
+ from pathlib import Path
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+ from llm import config
7
+ from core.init import run_init
8
+ from core.config_manager import ConfigManager as ConfigManagerClass
9
+ from ui.displays import show_help_table, show_provider_table, show_config_overview
10
+ from ui.banner import display_startup_banner
11
+
12
+ # Global ConfigManager instance
13
+ config_manager = ConfigManagerClass()
14
+
15
+
16
+ @dataclass
17
+ class CommandResult:
18
+ """Standardized command return type."""
19
+ status: str # "exit", "handled", or "continue"
20
+ replacement_input: Optional[str] = None # For /edit command
21
+
22
+ # Command handler functions
23
+
24
+ def _handle_exit(chat_manager, console, debug_mode_container, args):
25
+ """Handle exit command."""
26
+ return CommandResult(status="exit")
27
+
28
+
29
+ def _handle_help(chat_manager, console, debug_mode_container, args):
30
+ """Handle help command."""
31
+ show_help_table(console)
32
+ return CommandResult(status="handled")
33
+
34
+
35
+ def _handle_debug(chat_manager, console, debug_mode_container, args):
36
+ """Handle debug toggle command."""
37
+ debug_mode_container['debug'] = not debug_mode_container['debug']
38
+ status = "enabled" if debug_mode_container['debug'] else "disabled"
39
+ console.print(f"[yellow]Debug mode {status}[/yellow]")
40
+ return CommandResult(status="handled")
41
+
42
+ def _handle_compact(chat_manager, console, debug_mode_container, args):
43
+ """Handle manual context compaction."""
44
+ # Parse args for aggressive mode
45
+ aggressive = False
46
+ if args:
47
+ args_clean = args.strip().lower()
48
+ if args_clean in ('-a', '--aggressive'):
49
+ aggressive = True
50
+
51
+ # Show current context summary immediately using the same format as the status bar
52
+ num_messages = len(chat_manager.messages)
53
+ tokens_curr = chat_manager.token_tracker.current_context_tokens
54
+ console.print(
55
+ "Current context summary:"
56
+ f"\n Messages: {num_messages}"
57
+ f"\n Curr: {tokens_curr:,}"
58
+ )
59
+ console.print() # Spacer line
60
+
61
+ if aggressive:
62
+ console.print("[yellow]Aggressive mode: Compacting recent tool results too[/yellow]")
63
+ console.print()
64
+
65
+ result = chat_manager.compact_history(console=console, trigger="manual", aggressive=aggressive)
66
+ if not result:
67
+ console.print("[yellow]Nothing to compact.[/yellow]")
68
+ return CommandResult(status="handled")
69
+
70
+ mode_text = " (aggressive)" if aggressive else ""
71
+ console.print(
72
+ f"[green]Session reset{mode_text}: "
73
+ f"{result['before_tokens']:,} -> {result['after_tokens']:,} tokens[/green]"
74
+ )
75
+
76
+ # Show the compacted summary in debug mode
77
+ if debug_mode_container.get('debug') and 'summary' in result:
78
+ console.print()
79
+ console.print("[cyan]Compacted summary:[/cyan]")
80
+ console.print(f"[dim]{result['summary']}[/dim]")
81
+
82
+ return CommandResult(status="handled")
83
+
84
+
85
+ def _handle_mode(chat_manager, console, debug_mode_container, args):
86
+ """Handle interaction mode toggle command."""
87
+ new_mode = chat_manager.toggle_interaction_mode()
88
+
89
+ labels = {
90
+ "edit": "EDIT (Full Access)",
91
+ "plan": "PLAN (Read-Only)",
92
+ "learn": "LEARN (Read-Only)"
93
+ }
94
+ colors = {
95
+ "edit": "green",
96
+ "plan": "cyan",
97
+ "learn": "magenta"
98
+ }
99
+
100
+ label = labels.get(new_mode, new_mode.upper())
101
+ color = colors.get(new_mode, "white")
102
+
103
+ console.print(f"[{color}]Interaction Mode: {label}[/{color}]")
104
+ display_startup_banner(chat_manager.approve_mode, chat_manager.interaction_mode)
105
+ return CommandResult(status="handled")
106
+
107
+
108
+ def _handle_logging(chat_manager, console, debug_mode_container, args):
109
+ """Handle logging toggle command."""
110
+ is_enabled = chat_manager.toggle_logging()
111
+
112
+ if is_enabled:
113
+ console.print("[green]Conversation logging enabled.[/green]")
114
+ else:
115
+ console.print("[dim]Conversation logging disabled.[/dim]")
116
+
117
+ return CommandResult(status="handled")
118
+
119
+
120
+ def _handle_preplan(chat_manager, console, debug_mode_container, args):
121
+ """Handle pre-tool planning toggle command."""
122
+ new_state = not chat_manager.pre_tool_planning_enabled
123
+ chat_manager.pre_tool_planning_enabled = new_state
124
+ config_manager.set_pre_tool_planning(new_state)
125
+
126
+ # Update system prompt to reflect the change
127
+ chat_manager.update_system_prompt()
128
+
129
+ if new_state:
130
+ console.print("[cyan]Pre-tool planning: enabled[/cyan]")
131
+ else:
132
+ console.print("[dim]Pre-tool planning: disabled[/dim]")
133
+
134
+ return CommandResult(status="handled")
135
+
136
+
137
+ def _handle_config(chat_manager, console, debug_mode_container, args):
138
+ """Handle config overview command - display all settings."""
139
+ current_provider = getattr(chat_manager.client, 'provider', 'unknown')
140
+ show_config_overview(chat_manager, console, debug_mode_container, current_provider)
141
+ return CommandResult(status="handled")
142
+
143
+
144
+ def _handle_clear(chat_manager, console, debug_mode_container, args):
145
+ """Handle clear/reset command."""
146
+ # Display conversation cost for the previous chat
147
+ costs = config_manager.get_usage_costs()
148
+
149
+ # Display token summary for the previous chat
150
+ current_tokens = chat_manager.token_tracker.current_context_tokens
151
+ conv_in = chat_manager.token_tracker.conv_prompt_tokens
152
+ conv_out = chat_manager.token_tracker.conv_completion_tokens
153
+ conv_total = chat_manager.token_tracker.conv_total_tokens
154
+
155
+ console.print()
156
+ console.print("Conversation Summary:")
157
+ console.print(f" Current Context: {current_tokens:,} tokens")
158
+ console.print(f" In: {conv_in:,} tokens")
159
+ console.print(f" Out: {conv_out:,} tokens")
160
+ console.print(f" Total: {conv_total:,} tokens")
161
+
162
+ # Display cost if configured
163
+ if costs['in'] > 0 or costs['out'] > 0:
164
+ conv_cost = chat_manager.token_tracker.calculate_conversation_cost(costs['in'], costs['out'])
165
+ console.print(f" Cost: ${conv_cost['total_cost']:.4f}")
166
+
167
+ console.print()
168
+
169
+ chat_manager.reset_session()
170
+ display_startup_banner(chat_manager.approve_mode, chat_manager.interaction_mode)
171
+ return CommandResult(status="handled")
172
+
173
+
174
+ def _handle_provider(chat_manager, console, debug_mode_container, args):
175
+ """Handle provider switching command."""
176
+ if args:
177
+ provider = args.strip().lower()
178
+
179
+ # Validate provider name
180
+ if provider not in config.get_providers():
181
+ console.print(f"[red]Error: Unknown provider '{provider}'[/red]")
182
+ console.print(f"[dim]Available providers: {', '.join(config.get_providers())}[/dim]")
183
+ return CommandResult(status="handled")
184
+
185
+ # Switch provider
186
+ result = chat_manager.switch_provider(provider)
187
+
188
+ # Save provider choice after successful switch
189
+ if "Failed" not in result and "failed" not in result:
190
+ config_manager.set_provider(provider)
191
+ # Reload config and update client
192
+ chat_manager.reload_config()
193
+
194
+ # Clear screen and show banner after provider change
195
+ display_startup_banner(chat_manager.approve_mode, chat_manager.interaction_mode)
196
+ console.print(f"[yellow]{result}[/yellow]")
197
+
198
+ # Show helpful next steps
199
+ cfg = config.get_provider_config(provider)
200
+ if provider == "local":
201
+ if not cfg.get('model'):
202
+ console.print("[dim]Tip: Set model path with /model <path_to_gguf>[/dim]")
203
+ else:
204
+ if not cfg.get('api_key'):
205
+ console.print("[dim]Tip: Set API key with /key <your_api_key>[/dim]")
206
+ if not cfg.get('model'):
207
+ console.print("[dim]Tip: Set model with /model <model_name>[/dim]")
208
+ else:
209
+ current = getattr(chat_manager.client, 'provider', 'unknown')
210
+ show_provider_table(current, console)
211
+
212
+ return CommandResult(status="handled")
213
+
214
+
215
+ def _handle_model(chat_manager, console, debug_mode_container, args):
216
+ """Handle model setting command."""
217
+ if not args:
218
+ # Show current model for current provider
219
+ current_provider = getattr(chat_manager.client, 'provider', 'unknown')
220
+ cfg = config.get_provider_config(current_provider)
221
+ model = cfg.get('model') or cfg.get('api_model') or 'Not set'
222
+ console.print(f"[cyan]Current provider:[/cyan] {current_provider}")
223
+ console.print(f"[cyan]Current model:[/cyan] {model}")
224
+ return CommandResult(status="handled")
225
+
226
+ model = args.strip()
227
+
228
+ # Set model for current provider
229
+ current_provider = getattr(chat_manager.client, 'provider', 'unknown')
230
+
231
+ try:
232
+ backup_path = config_manager.set_model(current_provider, model)
233
+ console.print(f"[green]Model set to '{model}' for {current_provider} provider[/green]")
234
+ if backup_path:
235
+ console.print(f"[dim]Saved to config.json (backup: {backup_path.name})[/dim]")
236
+
237
+ # Reload config and update client
238
+ chat_manager.reload_config()
239
+ except ValueError as e:
240
+ console.print(f"[red]Error: {e}[/red]")
241
+ except Exception as e:
242
+ console.print(f"[red]Failed to set model: {e}[/red]")
243
+
244
+ return CommandResult(status="handled")
245
+
246
+
247
+ def _handle_key(chat_manager, console, debug_mode_container, args):
248
+ """Handle API key setting command."""
249
+ if not args:
250
+ # Show current API key status for current provider
251
+ current_provider = getattr(chat_manager.client, 'provider', 'unknown')
252
+ cfg = config.get_provider_config(current_provider)
253
+
254
+ if current_provider == "local":
255
+ console.print("[yellow]Local provider doesn't use API keys[/yellow]")
256
+ else:
257
+ api_key = cfg.get('api_key', '')
258
+ if api_key:
259
+ # Show masked API key
260
+ masked = api_key[:8] + "..." if len(api_key) > 8 else "***"
261
+ console.print(f"[cyan]Current provider:[/cyan] {current_provider}")
262
+ console.print(f"[cyan]API key:[/cyan] {masked}")
263
+ else:
264
+ console.print(f"[cyan]Current provider:[/cyan] {current_provider}")
265
+ console.print("[yellow]API key not set[/yellow]")
266
+ return CommandResult(status="handled")
267
+
268
+ api_key = args.strip()
269
+
270
+ # Set API key for current provider
271
+ current_provider = getattr(chat_manager.client, 'provider', 'unknown')
272
+
273
+ if current_provider == "local":
274
+ console.print("[yellow]Local provider doesn't use API keys[/yellow]")
275
+ return CommandResult(status="handled")
276
+
277
+ try:
278
+ backup_path = config_manager.set_api_key(current_provider, api_key)
279
+ console.print(f"[green]API key set for {current_provider} provider[/green]")
280
+ if backup_path:
281
+ console.print(f"[dim]Saved to config.json (backup: {backup_path.name})[/dim]")
282
+
283
+ # Reload config and update client
284
+ chat_manager.reload_config()
285
+ except ValueError as e:
286
+ console.print(f"[red]Error: {e}[/red]")
287
+ except Exception as e:
288
+ console.print(f"[red]Failed to set API key: {e}[/red]")
289
+
290
+ return CommandResult(status="handled")
291
+
292
+
293
+ def _handle_init(chat_manager, console, debug_mode_container, args):
294
+ """Handle init command."""
295
+ repo_root = Path.cwd()
296
+ run_init(repo_root, console)
297
+ chat_manager._init_messages(reset_totals=False) # Reload agents.md into context
298
+ return CommandResult(status="handled")
299
+
300
+
301
+ def _handle_edit(chat_manager, console, debug_mode_container, args):
302
+ """Handle external editor command for multi-line input.
303
+
304
+ Opens an external editor for composing prompts. After the editor closes,
305
+ the content is sent to the LLM.
306
+
307
+ Returns:
308
+ CommandResult: status="handled" if cancelled/failed
309
+ status="continue" with replacement_input to send to LLM
310
+ """
311
+ from utils.editor import open_editor_for_input
312
+
313
+ success, content = open_editor_for_input(
314
+ console,
315
+ debug_mode_container['debug']
316
+ )
317
+
318
+ if not success:
319
+ # Error already displayed by open_editor_for_input
320
+ return CommandResult(status="handled")
321
+
322
+ # Check if content is empty
323
+ if not content or not content.strip():
324
+ console.print("[yellow]Editor closed with no content - cancelling[/yellow]")
325
+ return CommandResult(status="handled")
326
+
327
+ # Show summary
328
+ lines = [line for line in content.split('\n') if line.strip()]
329
+ word_count = len(content.split())
330
+ console.print(f"[green]Received {len(lines)} lines ({word_count} words) from editor[/green]")
331
+
332
+ # Return continue status to pass content to LLM
333
+ return CommandResult(status="continue", replacement_input=content)
334
+
335
+
336
+
337
+
338
+
339
+ def _handle_usage(chat_manager, console, debug_mode_container, args):
340
+ """Handle usage command - show/calculate token costs or set cost rates."""
341
+ console.print()
342
+
343
+ # Get current model
344
+ current_model = getattr(chat_manager.client, 'model', '')
345
+
346
+ if args:
347
+ # Parse setting command: in|out <value>
348
+ parts = args.split()
349
+
350
+ if len(parts) != 2 or parts[0].lower() not in ['in', 'out']:
351
+ console.print("[red]Usage: /usage in|out <cost>[/red]")
352
+ console.print("[dim]Cost is per 1M tokens (e.g., 0.5 = $0.50 per 1M tokens)[/dim]")
353
+ console.print("[dim]Examples:[/dim]")
354
+ console.print(f"[dim] /usage in 1.00 - Set input cost for current model ({current_model})[/dim]")
355
+ console.print(f"[dim] /usage out 3.20 - Set output cost for current model ({current_model})[/dim]")
356
+ console.print()
357
+ return CommandResult(status="handled")
358
+
359
+ direction, value = parts
360
+ direction = direction.lower()
361
+
362
+ try:
363
+ cost = float(value)
364
+ if cost < 0:
365
+ console.print("[red]Error: Cost must be non-negative[/red]")
366
+ console.print()
367
+ return CommandResult(status="handled")
368
+ except ValueError:
369
+ console.print("[red]Error: Cost must be a valid number[/red]")
370
+ console.print()
371
+ return CommandResult(status="handled")
372
+
373
+ # Set appropriate cost for current model
374
+ # Get existing prices for the model
375
+ existing_prices = config_manager.get_model_price(current_model)
376
+ cost_in = existing_prices['in']
377
+ cost_out = existing_prices['out']
378
+
379
+ if direction == 'in':
380
+ cost_in = cost
381
+ elif direction == 'out':
382
+ cost_out = cost
383
+
384
+ backup_path = config_manager.set_model_price(current_model, cost_in, cost_out)
385
+
386
+ if direction == 'in':
387
+ console.print(f"[green]Model '{current_model}' input token cost set to ${cost:.6f} per 1M tokens[/green]")
388
+ else:
389
+ console.print(f"[green]Model '{current_model}' output token cost set to ${cost:.6f} per 1M tokens[/green]")
390
+
391
+ if backup_path:
392
+ console.print(f"[dim]Saved to config.json (backup: {backup_path.name})[/dim]")
393
+
394
+ console.print()
395
+ return CommandResult(status="handled")
396
+
397
+ # No args - show current usage stats
398
+ costs = config_manager.get_model_price(current_model)
399
+ tracker = chat_manager.token_tracker
400
+
401
+ # Display token counts
402
+ console.print(f"[cyan]Session Token Usage ({current_model}):[/cyan]")
403
+ console.print(f" Input tokens: {tracker.total_prompt_tokens:,}")
404
+ console.print(f" Output tokens: {tracker.total_completion_tokens:,}")
405
+ console.print(f" Total tokens: {tracker.total_tokens:,}")
406
+ console.print()
407
+
408
+
409
+ # Display costs if configured
410
+ if costs['in'] > 0 or costs['out'] > 0:
411
+ session_cost = tracker.calculate_session_cost(costs['in'], costs['out'])
412
+ console.print(f"[cyan]Session Cost ({current_model}):[/cyan]")
413
+
414
+ if costs['in'] > 0:
415
+ console.print(f" Input: ${session_cost['input_cost']:.6f} (${costs['in']:.6f}/1M tokens)")
416
+
417
+ if costs['out'] > 0:
418
+ console.print(f" Output: ${session_cost['output_cost']:.6f} (${costs['out']:.6f}/1M tokens)")
419
+
420
+ console.print(f" Total: ${session_cost['total_cost']:.6f}")
421
+ console.print()
422
+ console.print(f"[dim]Note: Costs are per-model. Switch model with /model to set different costs.[/dim]")
423
+ console.print()
424
+
425
+ else:
426
+ console.print(f"[yellow]Cost not configured for model '{current_model}'. Set with:[/yellow]")
427
+ console.print(f" /usage in <cost> - Set input token cost per 1M tokens")
428
+ console.print(f" /usage out <cost> - Set output token cost per 1M tokens")
429
+ console.print(f"[dim]Example: /usage in 2.50[/dim]")
430
+ console.print()
431
+
432
+ return CommandResult(status="handled")
433
+
434
+
435
+ # Command registry - maps command names to their handlers
436
+ _COMMAND_HANDLERS = {
437
+ "/exit": _handle_exit,
438
+ "/quit": _handle_exit,
439
+ "/help": _handle_help,
440
+ "/h": _handle_help,
441
+ "/debug": _handle_debug,
442
+ "/compact": _handle_compact,
443
+ "/mode": _handle_mode,
444
+ "/logging": _handle_logging,
445
+ "/clear": _handle_clear,
446
+ "/new": _handle_clear,
447
+ "/reset": _handle_clear,
448
+ "/provider": _handle_provider,
449
+ "/config": _handle_config,
450
+ "/init": _handle_init,
451
+ "/edit": _handle_edit,
452
+ "/e": _handle_edit,
453
+ "/usage": _handle_usage,
454
+ "/model": _handle_model,
455
+ "/key": _handle_key,
456
+ "/preplan": _handle_preplan,
457
+ }
458
+
459
+
460
+ def process_command(chat_manager, user_input, console, debug_mode_container):
461
+ """Process command and optionally return replacement content.
462
+
463
+ Args:
464
+ chat_manager: ChatManager instance
465
+ user_input: User's input string
466
+ console: Rich console for output
467
+ debug_mode_container: Dict with 'debug' key for debug mode state
468
+
469
+ Returns:
470
+ tuple: (status, replacement_content)
471
+ status: "exit" | "handled" | None
472
+ replacement_content: str to replace user_input, or None
473
+ """
474
+ # Parse command and arguments
475
+ parts = user_input.split(maxsplit=1)
476
+ cmd = parts[0].lower()
477
+ args = parts[1] if len(parts) > 1 else None
478
+
479
+ # Look up handler in registry
480
+ handler = _COMMAND_HANDLERS.get(cmd)
481
+ if handler:
482
+ result = handler(chat_manager, console, debug_mode_container, args)
483
+ return (result.status, result.replacement_input)
484
+ elif cmd.startswith('/'):
485
+ console.print(f"[red]Unknown command: {user_input}[/red]")
486
+ console.print("[dim]Type /help for available commands[/dim]")
487
+ return ("handled", None)
488
+
489
+ return (None, None)