@@ -233,7 +233,7 @@ private function registerAbilities(): void {
233233 'description ' => 'Maximum number of lines to return. ' ,
234234 ),
235235 ),
236- 'required ' => array ( 'repo ' , ' path ' ),
236+ 'required ' => array ( 'path ' ),
237237 ),
238238 'output_schema ' => array (
239239 'type ' => 'object ' ,
@@ -270,7 +270,7 @@ private function registerAbilities(): void {
270270 'description ' => 'Relative directory path within the repo (omit for root). ' ,
271271 ),
272272 ),
273- 'required ' => array ( ' repo ' ),
273+ 'required ' => array (),
274274 ),
275275 'output_schema ' => array (
276276 'type ' => 'object ' ,
@@ -331,7 +331,7 @@ private function registerAbilities(): void {
331331 'description ' => 'Number of surrounding lines to include for each match (default 0, max 10). ' ,
332332 ),
333333 ),
334- 'required ' => array ( 'repo ' , ' pattern ' ),
334+ 'required ' => array ( 'pattern ' ),
335335 ),
336336 'output_schema ' => array (
337337 'type ' => 'object ' ,
@@ -496,7 +496,7 @@ private function registerAbilities(): void {
496496 'description ' => 'File content to write. ' ,
497497 ),
498498 ),
499- 'required ' => array ( 'repo ' , ' path ' , 'content ' ),
499+ 'required ' => array ( 'path ' , 'content ' ),
500500 ),
501501 'output_schema ' => array (
502502 'type ' => 'object ' ,
@@ -538,12 +538,28 @@ private function registerAbilities(): void {
538538 'type ' => 'string ' ,
539539 'description ' => 'Replacement text. ' ,
540540 ),
541+ 'search ' => array (
542+ 'type ' => 'string ' ,
543+ 'description ' => 'Alias for old_string. ' ,
544+ ),
545+ 'replace ' => array (
546+ 'type ' => 'string ' ,
547+ 'description ' => 'Alias for new_string. ' ,
548+ ),
549+ 'old ' => array (
550+ 'type ' => 'string ' ,
551+ 'description ' => 'Alias for old_string. ' ,
552+ ),
553+ 'new ' => array (
554+ 'type ' => 'string ' ,
555+ 'description ' => 'Alias for new_string. ' ,
556+ ),
541557 'replace_all ' => array (
542558 'type ' => 'boolean ' ,
543559 'description ' => 'Replace all occurrences (default false). ' ,
544560 ),
545561 ),
546- 'required ' => array ( 'repo ' , ' path ' , ' old_string ' , ' new_string ' ),
562+ 'required ' => array ( 'path ' ),
547563 ),
548564 'output_schema ' => array (
549565 'type ' => 'object ' ,
@@ -617,7 +633,7 @@ private function registerAbilities(): void {
617633 'description ' => 'Workspace handle: `<repo>` (primary) or `<repo>@<branch-slug>` (worktree). ' ,
618634 ),
619635 ),
620- 'required ' => array ( ' name ' ),
636+ 'required ' => array (),
621637 ),
622638 'output_schema ' => array (
623639 'type ' => 'object ' ,
@@ -843,7 +859,7 @@ private function registerAbilities(): void {
843859 'description ' => 'Permit mutation on the primary checkout (default false). Worktrees are always allowed. ' ,
844860 ),
845861 ),
846- 'required ' => array ( 'repo ' , ' path ' ),
862+ 'required ' => array ( 'path ' ),
847863 ),
848864 'output_schema ' => array (
849865 'type ' => 'object ' ,
@@ -2275,6 +2291,7 @@ public static function showRepo( array $input ): array|\WP_Error {
22752291 * @return array Result.
22762292 */
22772293 public static function readFile ( array $ input ): array |\WP_Error {
2294+ $ input = self ::normalize_mounted_workspace_path_input ($ input , array ( 'repo ' ));
22782295 if ( RemoteWorkspaceBackend::should_handle () ) {
22792296 return ( new RemoteWorkspaceBackend () )->read_file (
22802297 $ input ['repo ' ] ?? '' ,
@@ -2304,6 +2321,7 @@ public static function readFile( array $input ): array|\WP_Error {
23042321 * @return array Result.
23052322 */
23062323 public static function listDirectory ( array $ input ): array |\WP_Error {
2324+ $ input = self ::normalize_mounted_workspace_path_input ($ input , array ( 'repo ' ));
23072325 if ( RemoteWorkspaceBackend::should_handle () ) {
23082326 return ( new RemoteWorkspaceBackend () )->list_directory (
23092327 $ input ['repo ' ] ?? '' ,
@@ -2327,6 +2345,7 @@ public static function listDirectory( array $input ): array|\WP_Error {
23272345 * @return array Result.
23282346 */
23292347 public static function grepFiles ( array $ input ): array |\WP_Error {
2348+ $ input = self ::normalize_mounted_workspace_path_input ($ input , array ( 'repo ' ));
23302349 if ( RemoteWorkspaceBackend::should_handle () ) {
23312350 return ( new RemoteWorkspaceBackend () )->grep (
23322351 $ input ['repo ' ] ?? '' ,
@@ -2433,12 +2452,24 @@ public static function writeFile( array $input ): array|\WP_Error {
24332452 * @return array Result.
24342453 */
24352454 public static function editFile ( array $ input ): array |\WP_Error {
2455+ $ input = self ::normalize_mounted_workspace_path_input ($ input , array ( 'repo ' ));
2456+ $ old_string = (string ) ( $ input ['old_string ' ] ?? $ input ['search ' ] ?? $ input ['old ' ] ?? '' );
2457+ $ new_string = (string ) ( $ input ['new_string ' ] ?? $ input ['replace ' ] ?? $ input ['new ' ] ?? '' );
2458+
2459+ if ( '' === $ old_string ) {
2460+ return new \WP_Error ('missing_old_string ' , 'old_string is required. ' , array ( 'status ' => 400 ));
2461+ }
2462+
2463+ if ( ! array_key_exists ('new_string ' , $ input ) && ! array_key_exists ('replace ' , $ input ) && ! array_key_exists ('new ' , $ input ) ) {
2464+ return new \WP_Error ('missing_new_string ' , 'new_string is required. ' , array ( 'status ' => 400 ));
2465+ }
2466+
24362467 if ( RemoteWorkspaceBackend::should_handle () ) {
24372468 return ( new RemoteWorkspaceBackend () )->edit_file (
24382469 $ input ['repo ' ] ?? '' ,
24392470 $ input ['path ' ] ?? '' ,
2440- $ input [ ' old_string ' ] ?? '' ,
2441- $ input [ ' new_string ' ] ?? '' ,
2471+ $ old_string ,
2472+ $ new_string ,
24422473 ! empty ($ input ['replace_all ' ])
24432474 );
24442475 }
@@ -2449,8 +2480,8 @@ public static function editFile( array $input ): array|\WP_Error {
24492480 return $ writer ->edit_file (
24502481 $ input ['repo ' ] ?? '' ,
24512482 $ input ['path ' ] ?? '' ,
2452- $ input [ ' old_string ' ] ?? '' ,
2453- $ input [ ' new_string ' ] ?? '' ,
2483+ $ old_string ,
2484+ $ new_string ,
24542485 ! empty ($ input ['replace_all ' ])
24552486 );
24562487 }
@@ -3309,6 +3340,100 @@ public static function workspaceCleanupCancel( array $input ): array|\WP_Error {
33093340 return ( new CleanupRunService () )->cancel ( (string ) ( $ input ['run_id ' ] ?? '' ));
33103341 }
33113342
3343+ /**
3344+ * Normalize mounted workspace absolute paths into ability-native inputs.
3345+ *
3346+ * @param array<string,mixed> $input Ability input.
3347+ * @param string[] $handle_keys Keys that can hold workspace handles.
3348+ * @return array<string,mixed>
3349+ */
3350+ private static function normalize_mounted_workspace_path_input ( array $ input , array $ handle_keys ): array {
3351+ $ workspace_root = defined ('DATAMACHINE_WORKSPACE_PATH ' ) ? self ::normalize_workspace_root ( (string ) DATAMACHINE_WORKSPACE_PATH ) : '' ;
3352+ if ( '' === $ workspace_root ) {
3353+ return $ input ;
3354+ }
3355+
3356+ foreach ( $ handle_keys as $ key ) {
3357+ if ( isset ($ input [ $ key ]) && is_string ($ input [ $ key ]) && self ::is_absolute_path ($ input [ $ key ]) ) {
3358+ $ parts = self ::split_workspace_root_path ($ input [ $ key ], $ workspace_root );
3359+ if ( null === $ parts ) {
3360+ return $ input ;
3361+ }
3362+
3363+ $ input [ $ key ] = $ parts ['repo ' ];
3364+ if ( '' !== $ parts ['path ' ] ) {
3365+ $ existing_path = isset ($ input ['path ' ]) && is_string ($ input ['path ' ]) ? trim ($ input ['path ' ], '/ ' ) : '' ;
3366+ $ input ['path ' ] = '' === $ existing_path ? $ parts ['path ' ] : $ parts ['path ' ] . '/ ' . $ existing_path ;
3367+ }
3368+ }
3369+ }
3370+
3371+ if ( isset ($ input ['path ' ]) && is_string ($ input ['path ' ]) && self ::is_absolute_path ($ input ['path ' ]) ) {
3372+ $ parts = self ::split_workspace_root_path ($ input ['path ' ], $ workspace_root );
3373+ if ( null === $ parts ) {
3374+ return $ input ;
3375+ }
3376+
3377+ $ current_handle = '' ;
3378+ foreach ( $ handle_keys as $ key ) {
3379+ if ( isset ($ input [ $ key ]) && is_string ($ input [ $ key ]) && '' !== trim ($ input [ $ key ]) ) {
3380+ $ current_handle = trim ($ input [ $ key ]);
3381+ break ;
3382+ }
3383+ }
3384+
3385+ if ( '' === $ current_handle && ! empty ($ handle_keys ) ) {
3386+ $ input [ $ handle_keys [0 ] ] = $ parts ['repo ' ];
3387+ }
3388+ if ( '' === $ current_handle || $ current_handle === $ parts ['repo ' ] ) {
3389+ $ input ['path ' ] = $ parts ['path ' ];
3390+ }
3391+ }
3392+
3393+ return $ input ;
3394+ }
3395+
3396+ private static function normalize_workspace_root ( string $ root ): string {
3397+ $ root = trim (str_replace ('\\' , '/ ' , trim ($ root )), '/ ' );
3398+ return '' === $ root ? '' : '/ ' . $ root ;
3399+ }
3400+
3401+ private static function is_absolute_path ( string $ path ): bool {
3402+ $ path = str_replace ('\\' , '/ ' , trim ($ path ));
3403+ return str_starts_with ($ path , '/ ' ) || (bool ) preg_match ('#^[a-zA-Z][a-zA-Z0-9+.-]*://# ' , $ path );
3404+ }
3405+
3406+ /**
3407+ * @return array{repo:string,path:string}|null
3408+ */
3409+ private static function split_workspace_root_path ( string $ path , string $ workspace_root ): ?array {
3410+ $ path = str_replace ('\\' , '/ ' , trim ($ path ));
3411+ if ( preg_match ('#^[a-zA-Z][a-zA-Z0-9+.-]*://# ' , $ path ) ) {
3412+ return null ;
3413+ }
3414+
3415+ $ root = rtrim ($ workspace_root , '/ ' );
3416+ if ( $ path !== $ root && ! str_starts_with ($ path , $ root . '/ ' ) ) {
3417+ return null ;
3418+ }
3419+
3420+ $ relative = ltrim (substr ($ path , strlen ($ root )), '/ ' );
3421+ if ( '' === $ relative ) {
3422+ return null ;
3423+ }
3424+
3425+ $ segments = array_values (array_filter (explode ('/ ' , $ relative ), static fn ( string $ segment ): bool => '' !== $ segment && '. ' !== $ segment ));
3426+ if ( empty ($ segments ) || in_array ('.. ' , $ segments , true ) ) {
3427+ return null ;
3428+ }
3429+
3430+ $ repo = array_shift ($ segments );
3431+ return array (
3432+ 'repo ' => $ repo ,
3433+ 'path ' => implode ('/ ' , $ segments ),
3434+ );
3435+ }
3436+
33123437 /**
33133438 * Read git log entries for a workspace repository.
33143439 *
@@ -3330,6 +3455,7 @@ public static function gitLog( array $input ): array|\WP_Error {
33303455 * @return array
33313456 */
33323457 public static function gitDiff ( array $ input ): array |\WP_Error {
3458+ $ input = self ::normalize_mounted_workspace_path_input ($ input , array ( 'name ' ));
33333459 if ( RemoteWorkspaceBackend::should_handle () ) {
33343460 return ( new RemoteWorkspaceBackend () )->git_diff (
33353461 $ input ['name ' ] ?? '' ,
0 commit comments