Coverage for sources / agentsmgr / renderers / claude.py: 70%
38 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 02:32 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 02:32 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
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#============================================================================#
21''' Claude Code renderer implementation.
23 Provides path resolution and targeting mode validation for Claude Code,
24 which supports both per-user and per-project configuration.
25'''
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
35class ClaudeRenderer( _RendererBase ):
36 ''' Renderer for Claude Code coder.
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 '''
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' ) )
49 def get_template_flavor( self, item_type: str ) -> str:
50 ''' Determines template flavor for Claude Code.
52 Claude uses markdown format for both commands and agents,
53 so always returns 'claude' flavor.
54 '''
55 return 'claude'
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.
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
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.
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 )
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.
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'
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.
120 Looks for coder entry in configuration coders array by name.
121 '''
122 return _extract_coder_configuration( configuration, self.name )
125_RENDERERS[ 'claude' ] = ClaudeRenderer( )