Coverage for sources / agentsmgr / renderers / claude.py: 70%

38 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-03 23:00 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Claude Code renderer implementation. 

22 

23 Provides path resolution and targeting mode validation for Claude Code, 

24 which supports both per-user and per-project configuration. 

25''' 

26 

27 

28from . import __ 

29from .base import RENDERERS as _RENDERERS 

30from .base import ExplicitTargetMode as _ExplicitTargetMode 

31from .base import RendererBase as _RendererBase 

32from .base import extract_coder_configuration as _extract_coder_configuration 

33 

34 

35class ClaudeRenderer( _RendererBase ): 

36 ''' Renderer for Claude Code coder. 

37 

38 Supports both per-user and per-project configuration modes. 

39 Per-user mode respects CLAUDE_CONFIG_DIR environment variable 

40 with fallback to configuration overrides and default location. 

41 ''' 

42 

43 name = 'claude' 

44 modes_available = frozenset( ( 'per-user', 'per-project' ) ) 

45 mode_default = 'per-project' 

46 memory_filename = 'CLAUDE.md' 

47 item_types_available = frozenset( ( 'commands', 'agents', 'skills' ) ) 

48 

49 def get_template_flavor( self, item_type: str ) -> str: 

50 ''' Determines template flavor for Claude Code. 

51 

52 Claude uses markdown format for both commands and agents, 

53 so always returns 'claude' flavor. 

54 ''' 

55 return 'claude' 

56 

57 def provide_project_symlinks( 

58 self, target: __.Path 

59 ) -> __.cabc.Sequence[ tuple[ __.Path, __.Path ] ]: 

60 ''' Provides symlinks required for Claude Code in per-project mode. 

61 

62 Claude requires base symlink plus .mcp.json symlink linking to 

63 the shared MCP server configuration file. 

64 ''' 

65 symlinks = list( super( ).provide_project_symlinks( target ) ) 

66 mcp_source = ( 

67 target / '.auxiliary' / 'configuration' / 'mcp-servers.json' ) 

68 mcp_link = target / '.mcp.json' 

69 symlinks.append( ( mcp_source, mcp_link ) ) 

70 return symlinks 

71 

72 def resolve_base_directory( 

73 self, 

74 mode: _ExplicitTargetMode, 

75 target: __.Path, 

76 configuration: __.cabc.Mapping[ str, __.typx.Any ], 

77 environment: __.cabc.Mapping[ str, str ], 

78 ) -> __.Path: 

79 ''' Resolves base output directory for Claude Code. 

80 

81 For per-project mode, returns .claude in project root. 

82 For per-user mode, respects precedence: CLAUDE_CONFIG_DIR 

83 environment variable, configuration file override, or default 

84 ~/.claude location. 

85 ''' 

86 self.validate_mode( mode ) 

87 if mode == 'per-project': 

88 return target / ".auxiliary/configuration/coders/claude" 

89 if mode == 'per-user': 89 ↛ 91line 89 didn't jump to line 91 because the condition on line 89 was always true

90 return self._resolve_user_directory( configuration, environment ) 

91 raise __.TargetModeNoSupport( self.name, mode ) 

92 

93 def _resolve_user_directory( 

94 self, 

95 configuration: __.cabc.Mapping[ str, __.typx.Any ], 

96 environment: __.cabc.Mapping[ str, str ], 

97 ) -> __.Path: 

98 ''' Resolves per-user directory following precedence rules. 

99 

100 Precedence order: 

101 1. CLAUDE_CONFIG_DIR environment variable 

102 2. Configuration file override (directory for this coder) 

103 3. Default ~/.claude location 

104 ''' 

105 if 'CLAUDE_CONFIG_DIR' in environment: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true

106 directory = __.Path( environment[ 'CLAUDE_CONFIG_DIR' ] ) 

107 return directory.expanduser( ) 

108 coder_configuration = self._extract_coder_configuration( 

109 configuration ) 

110 if 'directory' in coder_configuration: 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true

111 directory = __.Path( coder_configuration[ 'directory' ] ) 

112 return directory.expanduser( ) 

113 return __.Path.home( ) / '.claude' 

114 

115 def _extract_coder_configuration( 

116 self, configuration: __.cabc.Mapping[ str, __.typx.Any ] 

117 ) -> __.cabc.Mapping[ str, __.typx.Any ]: 

118 ''' Extracts configuration for this specific coder. 

119 

120 Looks for coder entry in configuration coders array by name. 

121 ''' 

122 return _extract_coder_configuration( configuration, self.name ) 

123 

124 

125_RENDERERS[ 'claude' ] = ClaudeRenderer( )