ccproxy-api 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  import os
5
5
  from pathlib import Path
6
- from typing import Any
6
+ from typing import Annotated, Any
7
7
 
8
8
  import typer
9
9
  import uvicorn
@@ -23,48 +23,25 @@ from ccproxy.config.settings import (
23
23
  )
24
24
  from ccproxy.core.async_utils import get_root_package_name
25
25
  from ccproxy.docker import (
26
- DockerEnv,
27
- DockerPath,
28
- DockerUserContext,
29
- DockerVolume,
30
26
  create_docker_adapter,
31
27
  )
32
28
 
33
29
  from ..docker import (
34
30
  _create_docker_adapter_from_settings,
35
31
  )
36
- from ..docker.params import (
37
- docker_arg_option,
38
- docker_env_option,
39
- docker_home_option,
40
- docker_image_option,
41
- docker_volume_option,
42
- docker_workspace_option,
43
- user_gid_option,
44
- user_mapping_option,
45
- user_uid_option,
46
- )
47
32
  from ..options.claude_options import (
48
33
  ClaudeOptions,
49
- allowed_tools_option,
50
- append_system_prompt_option,
51
- claude_cli_path_option,
52
- cwd_option,
53
- disallowed_tools_option,
54
- max_thinking_tokens_option,
55
- max_turns_option,
56
- permission_mode_option,
57
- permission_prompt_tool_name_option,
34
+ validate_claude_cli_path,
35
+ validate_cwd,
36
+ validate_max_thinking_tokens,
37
+ validate_max_turns,
38
+ validate_permission_mode,
58
39
  )
59
- from ..options.core_options import CoreOptions, config_option
60
- from ..options.security_options import SecurityOptions, auth_token_option
40
+ from ..options.security_options import SecurityOptions, validate_auth_token
61
41
  from ..options.server_options import (
62
42
  ServerOptions,
63
- host_option,
64
- log_file_option,
65
- log_level_option,
66
- port_option,
67
- reload_option,
43
+ validate_log_level,
44
+ validate_port,
68
45
  )
69
46
 
70
47
 
@@ -306,42 +283,236 @@ def _run_local_server(settings: Settings, cli_overrides: dict[str, Any]) -> None
306
283
 
307
284
  def api(
308
285
  # Configuration
309
- config: Path | None = config_option(),
286
+ config: Annotated[
287
+ Path | None,
288
+ typer.Option(
289
+ "--config",
290
+ "-c",
291
+ help="Path to configuration file (TOML, JSON, or YAML)",
292
+ exists=True,
293
+ file_okay=True,
294
+ dir_okay=False,
295
+ readable=True,
296
+ rich_help_panel="Configuration",
297
+ ),
298
+ ] = None,
310
299
  # Server options
311
- port: int | None = port_option(),
312
- host: str | None = host_option(),
313
- reload: bool | None = reload_option(),
314
- log_level: str | None = log_level_option(),
315
- log_file: str | None = log_file_option(),
300
+ port: Annotated[
301
+ int | None,
302
+ typer.Option(
303
+ "--port",
304
+ "-p",
305
+ help="Port to run the server on",
306
+ callback=validate_port,
307
+ rich_help_panel="Server Settings",
308
+ ),
309
+ ] = None,
310
+ host: Annotated[
311
+ str | None,
312
+ typer.Option(
313
+ "--host",
314
+ "-h",
315
+ help="Host to bind the server to",
316
+ rich_help_panel="Server Settings",
317
+ ),
318
+ ] = None,
319
+ reload: Annotated[
320
+ bool | None,
321
+ typer.Option(
322
+ "--reload/--no-reload",
323
+ help="Enable auto-reload for development",
324
+ rich_help_panel="Server Settings",
325
+ ),
326
+ ] = None,
327
+ log_level: Annotated[
328
+ str | None,
329
+ typer.Option(
330
+ "--log-level",
331
+ help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
332
+ callback=validate_log_level,
333
+ rich_help_panel="Server Settings",
334
+ ),
335
+ ] = None,
336
+ log_file: Annotated[
337
+ str | None,
338
+ typer.Option(
339
+ "--log-file",
340
+ help="Path to JSON log file. If specified, logs will be written to this file in JSON format",
341
+ rich_help_panel="Server Settings",
342
+ ),
343
+ ] = None,
316
344
  # Security options
317
- auth_token: str | None = auth_token_option(),
345
+ auth_token: Annotated[
346
+ str | None,
347
+ typer.Option(
348
+ "--auth-token",
349
+ help="Bearer token for API authentication",
350
+ callback=validate_auth_token,
351
+ rich_help_panel="Security Settings",
352
+ ),
353
+ ] = None,
318
354
  # Claude options
319
- max_thinking_tokens: int | None = max_thinking_tokens_option(),
320
- allowed_tools: str | None = allowed_tools_option(),
321
- disallowed_tools: str | None = disallowed_tools_option(),
322
- claude_cli_path: str | None = claude_cli_path_option(),
323
- append_system_prompt: str | None = append_system_prompt_option(),
324
- permission_mode: str | None = permission_mode_option(),
325
- max_turns: int | None = max_turns_option(),
326
- cwd: str | None = cwd_option(),
327
- permission_prompt_tool_name: str | None = permission_prompt_tool_name_option(),
355
+ max_thinking_tokens: Annotated[
356
+ int | None,
357
+ typer.Option(
358
+ "--max-thinking-tokens",
359
+ help="Maximum thinking tokens for Claude Code",
360
+ callback=validate_max_thinking_tokens,
361
+ rich_help_panel="Claude Settings",
362
+ ),
363
+ ] = None,
364
+ allowed_tools: Annotated[
365
+ str | None,
366
+ typer.Option(
367
+ "--allowed-tools",
368
+ help="List of allowed tools (comma-separated)",
369
+ rich_help_panel="Claude Settings",
370
+ ),
371
+ ] = None,
372
+ disallowed_tools: Annotated[
373
+ str | None,
374
+ typer.Option(
375
+ "--disallowed-tools",
376
+ help="List of disallowed tools (comma-separated)",
377
+ rich_help_panel="Claude Settings",
378
+ ),
379
+ ] = None,
380
+ claude_cli_path: Annotated[
381
+ str | None,
382
+ typer.Option(
383
+ "--claude-cli-path",
384
+ help="Path to Claude CLI executable",
385
+ callback=validate_claude_cli_path,
386
+ rich_help_panel="Claude Settings",
387
+ ),
388
+ ] = None,
389
+ append_system_prompt: Annotated[
390
+ str | None,
391
+ typer.Option(
392
+ "--append-system-prompt",
393
+ help="Additional system prompt to append",
394
+ rich_help_panel="Claude Settings",
395
+ ),
396
+ ] = None,
397
+ permission_mode: Annotated[
398
+ str | None,
399
+ typer.Option(
400
+ "--permission-mode",
401
+ help="Permission mode: default, acceptEdits, or bypassPermissions",
402
+ callback=validate_permission_mode,
403
+ rich_help_panel="Claude Settings",
404
+ ),
405
+ ] = None,
406
+ max_turns: Annotated[
407
+ int | None,
408
+ typer.Option(
409
+ "--max-turns",
410
+ help="Maximum conversation turns",
411
+ callback=validate_max_turns,
412
+ rich_help_panel="Claude Settings",
413
+ ),
414
+ ] = None,
415
+ cwd: Annotated[
416
+ str | None,
417
+ typer.Option(
418
+ "--cwd",
419
+ help="Working directory path",
420
+ callback=validate_cwd,
421
+ rich_help_panel="Claude Settings",
422
+ ),
423
+ ] = None,
424
+ permission_prompt_tool_name: Annotated[
425
+ str | None,
426
+ typer.Option(
427
+ "--permission-prompt-tool-name",
428
+ help="Permission prompt tool name",
429
+ rich_help_panel="Claude Settings",
430
+ ),
431
+ ] = None,
328
432
  # Core settings
329
- docker: bool = typer.Option(
330
- False,
331
- "--docker",
332
- "-d",
333
- help="Run API server using Docker instead of local execution",
334
- ),
433
+ docker: Annotated[
434
+ bool,
435
+ typer.Option(
436
+ "--docker",
437
+ "-d",
438
+ help="Run API server using Docker instead of local execution",
439
+ ),
440
+ ] = False,
335
441
  # Docker settings using shared parameters
336
- docker_image: str | None = docker_image_option(),
337
- docker_env: list[str] = docker_env_option(),
338
- docker_volume: list[str] = docker_volume_option(),
339
- docker_arg: list[str] = docker_arg_option(),
340
- docker_home: str | None = docker_home_option(),
341
- docker_workspace: str | None = docker_workspace_option(),
342
- user_mapping_enabled: bool | None = user_mapping_option(),
343
- user_uid: int | None = user_uid_option(),
344
- user_gid: int | None = user_gid_option(),
442
+ docker_image: Annotated[
443
+ str | None,
444
+ typer.Option(
445
+ "--docker-image",
446
+ help="Docker image to use (overrides configuration)",
447
+ rich_help_panel="Docker Settings",
448
+ ),
449
+ ] = None,
450
+ docker_env: Annotated[
451
+ list[str] | None,
452
+ typer.Option(
453
+ "--docker-env",
454
+ "-e",
455
+ help="Environment variables to pass to Docker container",
456
+ rich_help_panel="Docker Settings",
457
+ ),
458
+ ] = None,
459
+ docker_volume: Annotated[
460
+ list[str] | None,
461
+ typer.Option(
462
+ "--docker-volume",
463
+ "-v",
464
+ help="Volume mounts for Docker container",
465
+ rich_help_panel="Docker Settings",
466
+ ),
467
+ ] = None,
468
+ docker_arg: Annotated[
469
+ list[str] | None,
470
+ typer.Option(
471
+ "--docker-arg",
472
+ help="Additional arguments to pass to docker run",
473
+ rich_help_panel="Docker Settings",
474
+ ),
475
+ ] = None,
476
+ docker_home: Annotated[
477
+ str | None,
478
+ typer.Option(
479
+ "--docker-home",
480
+ help="Override the home directory for Docker",
481
+ rich_help_panel="Docker Settings",
482
+ ),
483
+ ] = None,
484
+ docker_workspace: Annotated[
485
+ str | None,
486
+ typer.Option(
487
+ "--docker-workspace",
488
+ help="Override the workspace directory for Docker",
489
+ rich_help_panel="Docker Settings",
490
+ ),
491
+ ] = None,
492
+ user_mapping_enabled: Annotated[
493
+ bool | None,
494
+ typer.Option(
495
+ "--user-mapping/--no-user-mapping",
496
+ help="Enable user mapping for Docker",
497
+ rich_help_panel="Docker Settings",
498
+ ),
499
+ ] = None,
500
+ user_uid: Annotated[
501
+ int | None,
502
+ typer.Option(
503
+ "--user-uid",
504
+ help="User UID for Docker user mapping",
505
+ rich_help_panel="Docker Settings",
506
+ ),
507
+ ] = None,
508
+ user_gid: Annotated[
509
+ int | None,
510
+ typer.Option(
511
+ "--user-gid",
512
+ help="User GID for Docker user mapping",
513
+ rich_help_panel="Docker Settings",
514
+ ),
515
+ ] = None,
345
516
  ) -> None:
346
517
  """
347
518
  Start the CCProxy API server.
@@ -496,26 +667,95 @@ def api(
496
667
 
497
668
 
498
669
  def claude(
499
- args: list[str] | None = typer.Argument(
500
- default=None,
501
- help="Arguments to pass to claude CLI (e.g. --version, doctor, config)",
502
- ),
503
- docker: bool = typer.Option(
504
- False,
505
- "--docker",
506
- "-d",
507
- help="Run claude command from docker image instead of local CLI",
508
- ),
670
+ args: Annotated[
671
+ list[str] | None,
672
+ typer.Argument(
673
+ help="Arguments to pass to claude CLI (e.g. --version, doctor, config)",
674
+ ),
675
+ ] = None,
676
+ docker: Annotated[
677
+ bool,
678
+ typer.Option(
679
+ "--docker",
680
+ "-d",
681
+ help="Run claude command from docker image instead of local CLI",
682
+ ),
683
+ ] = False,
509
684
  # Docker settings using shared parameters
510
- docker_image: str | None = docker_image_option(),
511
- docker_env: list[str] = docker_env_option(),
512
- docker_volume: list[str] = docker_volume_option(),
513
- docker_arg: list[str] = docker_arg_option(),
514
- docker_home: str | None = docker_home_option(),
515
- docker_workspace: str | None = docker_workspace_option(),
516
- user_mapping_enabled: bool | None = user_mapping_option(),
517
- user_uid: int | None = user_uid_option(),
518
- user_gid: int | None = user_gid_option(),
685
+ docker_image: Annotated[
686
+ str | None,
687
+ typer.Option(
688
+ "--docker-image",
689
+ help="Docker image to use (overrides configuration)",
690
+ rich_help_panel="Docker Settings",
691
+ ),
692
+ ] = None,
693
+ docker_env: Annotated[
694
+ list[str] | None,
695
+ typer.Option(
696
+ "--docker-env",
697
+ "-e",
698
+ help="Environment variables to pass to Docker container",
699
+ rich_help_panel="Docker Settings",
700
+ ),
701
+ ] = None,
702
+ docker_volume: Annotated[
703
+ list[str] | None,
704
+ typer.Option(
705
+ "--docker-volume",
706
+ "-v",
707
+ help="Volume mounts for Docker container",
708
+ rich_help_panel="Docker Settings",
709
+ ),
710
+ ] = None,
711
+ docker_arg: Annotated[
712
+ list[str] | None,
713
+ typer.Option(
714
+ "--docker-arg",
715
+ help="Additional arguments to pass to docker run",
716
+ rich_help_panel="Docker Settings",
717
+ ),
718
+ ] = None,
719
+ docker_home: Annotated[
720
+ str | None,
721
+ typer.Option(
722
+ "--docker-home",
723
+ help="Override the home directory for Docker",
724
+ rich_help_panel="Docker Settings",
725
+ ),
726
+ ] = None,
727
+ docker_workspace: Annotated[
728
+ str | None,
729
+ typer.Option(
730
+ "--docker-workspace",
731
+ help="Override the workspace directory for Docker",
732
+ rich_help_panel="Docker Settings",
733
+ ),
734
+ ] = None,
735
+ user_mapping_enabled: Annotated[
736
+ bool | None,
737
+ typer.Option(
738
+ "--user-mapping/--no-user-mapping",
739
+ help="Enable user mapping for Docker",
740
+ rich_help_panel="Docker Settings",
741
+ ),
742
+ ] = None,
743
+ user_uid: Annotated[
744
+ int | None,
745
+ typer.Option(
746
+ "--user-uid",
747
+ help="User UID for Docker user mapping",
748
+ rich_help_panel="Docker Settings",
749
+ ),
750
+ ] = None,
751
+ user_gid: Annotated[
752
+ int | None,
753
+ typer.Option(
754
+ "--user-gid",
755
+ help="User GID for Docker user mapping",
756
+ rich_help_panel="Docker Settings",
757
+ ),
758
+ ] = None,
519
759
  ) -> None:
520
760
  """
521
761
  Execute claude CLI commands directly.
ccproxy/cli/main.py CHANGED
@@ -1,10 +1,9 @@
1
1
  """Main entry point for CCProxy API Server."""
2
2
 
3
- import json
4
3
  import os
5
4
  import secrets
6
5
  from pathlib import Path
7
- from typing import Any, Optional, cast
6
+ from typing import Annotated, Any, Optional, cast
8
7
 
9
8
  import typer
10
9
  from click import get_current_context
@@ -26,14 +25,10 @@ from ccproxy.config.settings import (
26
25
  )
27
26
  from ccproxy.core.async_utils import get_package_dir, get_root_package_name
28
27
  from ccproxy.core.logging import setup_logging
29
- from ccproxy.models.responses import (
30
- PermissionToolAllowResponse,
31
- PermissionToolDenyResponse,
32
- )
33
28
 
34
29
  from .commands.auth import app as auth_app
35
30
  from .commands.config import app as config_app
36
- from .commands.serve import api, get_config_path_from_context
31
+ from .commands.serve import api
37
32
 
38
33
 
39
34
  def version_callback(value: bool) -> None:
@@ -49,6 +44,7 @@ app = typer.Typer(
49
44
  add_completion=True,
50
45
  no_args_is_help=False,
51
46
  pretty_exceptions_enable=False,
47
+ invoke_without_command=True,
52
48
  )
53
49
 
54
50
  # Logger will be configured by configuration manager
@@ -59,30 +55,42 @@ logger = get_logger(__name__)
59
55
  @app.callback()
60
56
  def app_main(
61
57
  ctx: typer.Context,
62
- version: bool = typer.Option(
63
- False,
64
- "--version",
65
- "-V",
66
- callback=version_callback,
67
- is_eager=True,
68
- help="Show version and exit.",
69
- ),
70
- config: Path | None = typer.Option(
71
- None,
72
- "--config",
73
- "-c",
74
- help="Path to configuration file (TOML, JSON, or YAML)",
75
- exists=True,
76
- file_okay=True,
77
- dir_okay=False,
78
- readable=True,
79
- ),
58
+ version: Annotated[
59
+ bool,
60
+ typer.Option(
61
+ "--version",
62
+ "-V",
63
+ callback=version_callback,
64
+ is_eager=True,
65
+ help="Show version and exit.",
66
+ ),
67
+ ] = False,
68
+ config: Annotated[
69
+ Path | None,
70
+ typer.Option(
71
+ "--config",
72
+ "-c",
73
+ help="Path to configuration file (TOML, JSON, or YAML)",
74
+ exists=True,
75
+ file_okay=True,
76
+ dir_okay=False,
77
+ readable=True,
78
+ ),
79
+ ] = None,
80
80
  ) -> None:
81
81
  """CCProxy API Server - Anthropic and OpenAI compatible interface for Claude."""
82
82
  # Store config path for commands to use
83
83
  ctx.ensure_object(dict)
84
84
  ctx.obj["config_path"] = config
85
85
 
86
+ # If no command is invoked, run the serve command by default
87
+ if ctx.invoked_subcommand is None:
88
+ # Import here to avoid circular imports
89
+ from .commands.serve import api
90
+
91
+ # Invoke the serve command
92
+ ctx.invoke(api)
93
+
86
94
 
87
95
  # Register config command
88
96
  app.add_typer(config_app)
@@ -96,92 +104,6 @@ app.command(name="serve")(api)
96
104
  # Claude command removed - functionality moved to serve command
97
105
 
98
106
 
99
- @app.command()
100
- def permission_tool(
101
- tool_name: str = typer.Argument(
102
- ..., help="Name of the tool to check permissions for"
103
- ),
104
- tool_input: str = typer.Argument(..., help="JSON string of the tool input"),
105
- ) -> None:
106
- """
107
- MCP permission prompt tool for Claude Code SDK.
108
-
109
- This tool is used by the Claude Code SDK to check permissions for tool calls.
110
- It returns a JSON response indicating whether the tool call should be allowed or denied.
111
-
112
- Response format:
113
- - Allow: {"behavior": "allow", "updatedInput": {...}}
114
- - Deny: {"behavior": "deny", "message": "reason"}
115
-
116
- Examples:
117
- ccproxy permission_tool "bash" '{"command": "ls -la"}'
118
- ccproxy permission_tool "edit_file" '{"path": "/etc/passwd", "content": "..."}'
119
- """
120
- toolkit = get_rich_toolkit()
121
-
122
- try:
123
- # Parse the tool input JSON
124
- try:
125
- input_data = json.loads(tool_input)
126
- except json.JSONDecodeError as e:
127
- response = PermissionToolDenyResponse(message=f"Invalid JSON input: {e}")
128
- toolkit.print(response.model_dump_json(by_alias=True), tag="result")
129
- raise typer.Exit(1) from e
130
-
131
- # Load settings to get permission configuration
132
- settings = config_manager.load_settings(
133
- config_path=get_config_path_from_context()
134
- )
135
-
136
- # Basic permission checking logic
137
- # This can be extended with more sophisticated rules
138
-
139
- # Check for potentially dangerous commands
140
- dangerous_patterns = [
141
- "rm -rf",
142
- "sudo",
143
- "passwd",
144
- "chmod 777",
145
- "/etc/passwd",
146
- "/etc/shadow",
147
- "format",
148
- "mkfs",
149
- ]
150
-
151
- # Convert input to string for pattern matching
152
- input_str = json.dumps(input_data).lower()
153
-
154
- # Check for dangerous patterns
155
- for pattern in dangerous_patterns:
156
- if pattern in input_str:
157
- response = PermissionToolDenyResponse(
158
- message=f"Tool call contains potentially dangerous pattern: {pattern}"
159
- )
160
- toolkit.print(response.model_dump_json(by_alias=True), tag="result")
161
- return
162
-
163
- # Check for specific tool restrictions
164
- restricted_tools = {"exec", "system", "shell", "subprocess"}
165
-
166
- if tool_name.lower() in restricted_tools:
167
- response = PermissionToolDenyResponse(
168
- message=f"Tool {tool_name} is restricted for security reasons"
169
- )
170
- toolkit.print(response.model_dump_json(by_alias=True), tag="result")
171
- return
172
-
173
- # Allow the tool call with original input
174
- allow_response = PermissionToolAllowResponse(updated_input=input_data)
175
- toolkit.print(allow_response.model_dump_json(by_alias=True), tag="result")
176
-
177
- except Exception as e:
178
- error_response = PermissionToolDenyResponse(
179
- message=f"Error processing permission request: {e}"
180
- )
181
- toolkit.print(error_response.model_dump_json(by_alias=True), tag="result")
182
- raise typer.Exit(1) from e
183
-
184
-
185
107
  def main() -> None:
186
108
  """Entry point for the CLI application."""
187
109
  app()