diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae38f88..30bfa50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,7 @@ jobs: fish fish/tests/w4b-screenshots.fish fish fish/tests/w5a-hremote.fish fish fish/tests/w5b-herdr-ssh.fish + fish fish/tests/w6-agent-helpers.fish - name: Validate JSON configs shell: bash diff --git a/README.md b/README.md index 649867b..3353f52 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,21 @@ Para validar con `HOME`, `PATH` y comandos macOS falsos aislados: /opt/homebrew/bin/fish fish/tests/w4b-screenshots.fish ``` +## Helpers Fish de agentes (W6) + +**ADVERTENCIA:** por elección explícita del owner, `cc` ejecuta Claude Code con `--dangerously-skip-permissions` y `oc`/`ocb` ejecutan OpenCode con `--auto`; estos shortcuts intencionalmente omiten o autoaprueban permisos. W6 agrega `cc`, `oc`, `ocb`, `ccx`, `ccd` y `ccclip` como helpers Fish opt-in en el directorio validado; `ccx` entrega el contexto por stdin y `ccclip` escribe al clipboard solo al invocarse. No incluye `ccb`. + +| Área | Owner en W6 | +| --- | --- | +| Helpers y soporte privado | `fish/functions/{_cortex_resolve_target,_cortex_run_agent,cc,oc,ocb,ccx,ccd,ccclip}.fish` | +| Home Manager | Mapea cada una de esas funciones de forma explícita; no administra binarios de agentes, clipboard, worktrees, estado, historial ni configuración de proveedores | + +Para validar con agentes y clipboard falsos aislados: + +```bash +/opt/homebrew/bin/fish fish/tests/w6-agent-helpers.fish +``` + ## Estructura ``` diff --git a/fish/functions/_cortex_resolve_target.fish b/fish/functions/_cortex_resolve_target.fish new file mode 100644 index 0000000..72eeb40 --- /dev/null +++ b/fish/functions/_cortex_resolve_target.fish @@ -0,0 +1,11 @@ +function _cortex_resolve_target + set -l target "$argv[1]" + test -n "$target"; or set target . + + if not test -d "$target" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + + cd "$target"; and pwd +end diff --git a/fish/functions/_cortex_run_agent.fish b/fish/functions/_cortex_run_agent.fish new file mode 100644 index 0000000..ff5cefd --- /dev/null +++ b/fish/functions/_cortex_run_agent.fish @@ -0,0 +1,11 @@ +function _cortex_run_agent + set -l target "$argv[1]" + set -e argv[1] + set -l original_dir "$PWD" + + cd "$target"; or return 1 + command $argv + set -l agent_status $status + cd "$original_dir"; or return 1 + return "$agent_status" +end diff --git a/fish/functions/cc.fish b/fish/functions/cc.fish new file mode 100644 index 0000000..99531aa --- /dev/null +++ b/fish/functions/cc.fish @@ -0,0 +1,5 @@ +# Usage: cc [path] +function cc + set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 + _cortex_run_agent "$resolved" claude --dangerously-skip-permissions +end diff --git a/fish/functions/ccclip.fish b/fish/functions/ccclip.fish new file mode 100644 index 0000000..db8f35b --- /dev/null +++ b/fish/functions/ccclip.fish @@ -0,0 +1,43 @@ +# Usage: ccclip [-n|--line-numbers] +function ccclip + if test (count $argv) -eq 0 + printf 'Uso: ccclip [archivo2 ...] [-n|--line-numbers]\n' >&2 + return 1 + end + + set -l with_numbers false + set -l files + for arg in $argv + switch "$arg" + case -n --line-numbers + set with_numbers true + case '*' + set -a files "$arg" + end + end + + begin + for file in $files + if not test -f "$file" + printf 'Archivo no encontrado: %s\n' "$file" >&2 + continue + end + + set -l extension (path extension "$file" | string trim -c .) + printf '```%s\n// File: %s\n' "$extension" "$file" + set -l content + if test "$with_numbers" = true + set content (nl -ba "$file" | string collect) + else + set content (command cat "$file" | string collect) + end + printf '%s\n```\n\n' "$content" + end + end | pbcopy + set -l clipboard_status $pipestatus[-1] + if test "$clipboard_status" -ne 0 + return "$clipboard_status" + end + + printf 'Contexto copiado al clipboard (%d archivos)\n' (count $files) +end diff --git a/fish/functions/ccd.fish b/fish/functions/ccd.fish new file mode 100644 index 0000000..9f3fd98 --- /dev/null +++ b/fish/functions/ccd.fish @@ -0,0 +1,16 @@ +# Usage: ccd [subpath] +function ccd + set -l subpath "$argv[1]" + set -l workspace "$WORKSPACE_DIR" + test -n "$workspace"; or set workspace "$HOME/dev" + set -l target "$workspace" + test -n "$subpath"; and set target "$workspace/$subpath" + + if test -d "$target" + cd "$target" + printf 'Navegando a: %s\n' "$target" + else + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end +end diff --git a/fish/functions/ccx.fish b/fish/functions/ccx.fish new file mode 100644 index 0000000..7292837 --- /dev/null +++ b/fish/functions/ccx.fish @@ -0,0 +1,14 @@ +# Usage: ccx [path] +function ccx + set -l context "$argv[1]" + if test -z "$context" + printf 'Uso: ccx [directorio]\n' >&2 + return 1 + end + + set -l resolved (_cortex_resolve_target "$argv[2]"); or return 1 + begin + printf '%s\n' "$context" | _cortex_run_agent "$resolved" claude + return $pipestatus[-1] + end +end diff --git a/fish/functions/oc.fish b/fish/functions/oc.fish new file mode 100644 index 0000000..c0aaaa0 --- /dev/null +++ b/fish/functions/oc.fish @@ -0,0 +1,9 @@ +# Usage: oc [path] +function oc + set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 + set -l flags + if set -q OPENCODE_DEFAULT_FLAGS; and test -n "$OPENCODE_DEFAULT_FLAGS" + set flags (string replace -ra '[[:space:]]+' \n -- "$OPENCODE_DEFAULT_FLAGS") + end + _cortex_run_agent "$resolved" opencode --auto $flags +end diff --git a/fish/functions/ocb.fish b/fish/functions/ocb.fish new file mode 100644 index 0000000..694b2c3 --- /dev/null +++ b/fish/functions/ocb.fish @@ -0,0 +1,4 @@ +# Usage: ocb [path] +function ocb + oc $argv +end diff --git a/fish/tests/w6-agent-helpers.fish b/fish/tests/w6-agent-helpers.fish new file mode 100644 index 0000000..9874454 --- /dev/null +++ b/fish/tests/w6-agent-helpers.fish @@ -0,0 +1,139 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 +function _cleanup_w6 --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function contains + string match -q "*$argv[2]*" -- "$argv[1]"; or fail "$argv[3]" +end + +set -g home "$workspace/home" +set -g bin "$workspace/bin" +set -g repo "$workspace/repo" +set -g target "$workspace/target dir" +set -g workspace_dir "$workspace/workspace dir" +mkdir -p "$home/dev" "$bin" "$repo" "$target" "$workspace_dir/sub dir" +printf '#!/bin/sh\nprintf "claude|%%s|cwd=%%s|stdin=" "$*" "$PWD" >> "$W6_LOG"\nwhile IFS= read -r line; do printf "%%s\\n" "$line" >> "$W6_LOG"; done\nprintf "\\n" >> "$W6_LOG"\nexit "${W6_CLAUDE_STATUS:-0}"\n' > "$bin/claude" +printf '#!/bin/sh\nprintf "opencode|%%s|cwd=%%s\\n" "$*" "$PWD" >> "$W6_LOG"\nexit "${W6_OPENCODE_STATUS:-0}"\n' > "$bin/opencode" +printf '#!/bin/sh\nprintf "pbcopy|" >> "$W6_LOG"\nwhile IFS= read -r line; do printf "%%s\\n" "$line" >> "$W6_LOG"; done\nprintf "\\n" >> "$W6_LOG"\nexit "${W6_PBCOPY_STATUS:-0}"\n' > "$bin/pbcopy" +printf '#!/bin/sh\nfor file; do while IFS= read -r line || [ -n "$line" ]; do printf "%%s\\n" "$line"; done < "$file"; done\n' > "$bin/cat" +printf '%s\n' '#!/bin/sh' 'for file; do' ' case "$file" in -*) continue ;; esac' ' i=0' ' while IFS= read -r line || [ -n "$line" ]; do' ' i=$((i + 1))' ' printf "%6s\\t%s\\n" "$i" "$line"' ' done < "$file"' 'done' > "$bin/nl" +chmod +x "$bin"/* +printf 'alpha\n' > "$target/code file.fish" +printf 'beta\n' > "$target/other.txt" +printf '' > "$target/empty.txt" + +function run_w6 + env -i HOME="$home" PATH="$bin:/usr/bin:/bin" W6_LOG="$workspace/log" \ + W6_CLAUDE_STATUS="$W6_CLAUDE_STATUS" W6_OPENCODE_STATUS="$W6_OPENCODE_STATUS" W6_PBCOPY_STATUS="$W6_PBCOPY_STATUS" OPENCODE_DEFAULT_FLAGS="$OPENCODE_DEFAULT_FLAGS" WORKSPACE_DIR="$WORKSPACE_DIR" \ + "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$functions_dir" "$repo" $argv +end +function run_w6_unavailable + env -i HOME="$home" PATH="/usr/bin:/bin" "$fish_bin" --no-config \ + -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$functions_dir" "$repo" $argv +end + +printf '' > "$workspace/log" +run_w6 cc; or fail 'cc must use the current directory without arguments' +string match -q -- "claude|--dangerously-skip-permissions|cwd=$repo|stdin=" (string collect <"$workspace/log"); or fail 'cc default command order' +printf '' > "$workspace/log" +run_w6 cc "$target"; or fail 'cc must succeed' +set -l log (string collect <"$workspace/log") +string match -q -- "claude|--dangerously-skip-permissions|cwd=$target|stdin=" "$log"; or fail 'cc target command order' + +printf '' > "$workspace/log" +run_w6 oc "$target"; or fail 'oc must succeed without defaults' +string match -q -- "opencode|--auto|cwd=$target" (string collect <"$workspace/log"); or fail 'oc default command order' +printf '' > "$workspace/log" +set -gx OPENCODE_DEFAULT_FLAGS (printf '%s\t%s\n%s' --model test --plain | string collect --no-trim-newlines) +run_w6 oc "$target"; or fail 'oc must tokenize shell whitespace in defaults' +string match -q -- "opencode|--auto --model test --plain|cwd=$target" (string collect <"$workspace/log"); or fail 'oc tokenizes defaults after one auto flag' +set -gx OPENCODE_DEFAULT_FLAGS '--model test --plain' +printf '' > "$workspace/log" +run_w6 ocb "$target"; or fail 'ocb must succeed' +string match -q -- "opencode|--auto --model test --plain|cwd=$target" (string collect <"$workspace/log"); or fail 'ocb forwards defaults after one auto flag' +set -e OPENCODE_DEFAULT_FLAGS +set -gx W6_OPENCODE_STATUS 24 +run_w6 oc "$target" >/dev/null 2>&1; and fail 'opencode status must propagate' +set -e W6_OPENCODE_STATUS + +printf '' > "$workspace/log" +run_w6 ccx 'initial context with spaces' "$target"; or fail 'ccx must succeed' +contains (string collect <"$workspace/log") "claude||cwd=$target|stdin=initial context with spaces" 'ccx forwards context through stdin' +if run_w6 ccx >"$workspace/ccx.out" 2>&1 + fail 'ccx without context must fail' +end +contains (string collect <"$workspace/ccx.out") 'Uso: ccx' 'ccx usage' + +if run_w6 cc "$workspace/missing" >"$workspace/missing.out" 2>&1 + fail 'missing agent target must fail' +end +contains (string collect <"$workspace/missing.out") 'Directorio no encontrado' 'missing target error' +set -gx W6_CLAUDE_STATUS 23 +run_w6 cc "$target" >/dev/null 2>&1; and fail 'claude status must propagate' +set -e W6_CLAUDE_STATUS +set -gx W6_CLAUDE_STATUS 25 +run_w6 ccx context "$target" >/dev/null 2>&1; and fail 'ccx status must propagate' +set -e W6_CLAUDE_STATUS +if run_w6_unavailable cc "$target" >"$workspace/unavailable.out" 2>&1 + fail 'unavailable claude must fail' +end +contains (string collect <"$workspace/unavailable.out") 'claude' 'unavailable claude identifies command' + +set -gx WORKSPACE_DIR '' +set -l ccd_default (run_w6 ccd) +contains "$ccd_default" "Navegando a: $home/dev" 'ccd default directory' +set -gx WORKSPACE_DIR "$workspace_dir" +set -l ccd_output (run_w6 ccd 'sub dir') +contains "$ccd_output" "Navegando a: $workspace_dir/sub dir" 'ccd uses workspace override' +source "$functions_dir/ccd.fish" +cd "$repo" +ccd 'sub dir' >/dev/null; or fail 'ccd must change the invoking Fish process directory' +test "$PWD" = "$workspace_dir/sub dir"; or fail 'ccd persists PWD in the invoking Fish process' +if run_w6 ccd missing >"$workspace/ccd.out" 2>&1 + fail 'ccd missing directory must fail' +end +contains (string collect <"$workspace/ccd.out") 'Directorio no encontrado' 'ccd validates target' +set -e WORKSPACE_DIR + +printf '' > "$workspace/log" +run_w6 ccclip "$target/code file.fish" -n; or fail 'ccclip must copy content' +set log (string collect <"$workspace/log") +contains "$log" "pbcopy|```fish" 'ccclip fences extension' +contains "$log" "// File: $target/code file.fish" 'ccclip preserves spaced path' +contains "$log" '1 alpha' 'ccclip includes line numbers' +printf '' > "$workspace/log" +run_w6 ccclip "$target/code file.fish"; or fail 'ccclip must copy newline-terminated content' +set log (string collect <"$workspace/log") +set -l expected_fence (string join \n alpha '```' | string collect) +contains "$log" "$expected_fence" 'ccclip trims trailing linefeeds before the closing fence' +printf '' > "$workspace/log" +run_w6 ccclip "$target/empty.txt"; or fail 'ccclip must copy empty files' +printf '' > "$workspace/log" +run_w6 ccclip "$workspace/missing"; or fail 'ccclip empty context preserves successful clipboard status' +contains (string collect <"$workspace/log") 'pbcopy|' 'ccclip hands empty context to clipboard' +set -gx W6_PBCOPY_STATUS 31 +run_w6 ccclip "$target/other.txt" >/dev/null 2>&1; and fail 'clipboard failure must propagate' +set -e W6_PBCOPY_STATUS +if run_w6 ccclip >"$workspace/clip.out" 2>&1 + fail 'ccclip without files must fail' +end +contains (string collect <"$workspace/clip.out") 'Uso: ccclip' 'ccclip usage' + +for file in "$functions_dir/_cortex_resolve_target.fish" "$functions_dir/_cortex_run_agent.fish" "$functions_dir/ocb.fish" "$functions_dir/ccx.fish" "$functions_dir/ccd.fish" "$functions_dir/ccclip.fish" + set -l source (string collect < "$file") + string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv|--yes)' "$source"; and fail "unexpected unsafe W6 source: $file" +end + +printf 'PASS: Fish W6 agent helpers\n' diff --git a/nix/home.nix b/nix/home.nix index 1f056ce..c4b0cb9 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -74,5 +74,13 @@ "fish/functions/last.fish".source = ../fish/functions/last.fish; "fish/functions/ssd.fish".source = ../fish/functions/ssd.fish; "fish/functions/imgclip.fish".source = ../fish/functions/imgclip.fish; + "fish/functions/_cortex_resolve_target.fish".source = ../fish/functions/_cortex_resolve_target.fish; + "fish/functions/_cortex_run_agent.fish".source = ../fish/functions/_cortex_run_agent.fish; + "fish/functions/cc.fish".source = ../fish/functions/cc.fish; + "fish/functions/oc.fish".source = ../fish/functions/oc.fish; + "fish/functions/ocb.fish".source = ../fish/functions/ocb.fish; + "fish/functions/ccx.fish".source = ../fish/functions/ccx.fish; + "fish/functions/ccd.fish".source = ../fish/functions/ccd.fish; + "fish/functions/ccclip.fish".source = ../fish/functions/ccclip.fish; }; }