add gcpropose helper command

This commit is contained in:
RingOfStorms (Joshua Bell) 2026-01-04 22:36:24 -06:00
parent f86b8085c2
commit effa01310b
5 changed files with 210 additions and 35 deletions

View file

@ -33,7 +33,9 @@ in
controlMaster = "no";
controlPath = "~/.ssh/master-%r@%n:%p";
controlPersist = "no";
StrictHostKeyChecking = "accept-new";
extraOptions = {
StrictHostKeyChecking = "accept-new";
};
};
# EXTERNAL

View file

@ -22,6 +22,7 @@ with lib;
bx = "branchdel";
b = "branch";
bs = "branching_setup";
gcp = "gcpropose";
};
environment.shellInit = lib.concatStringsSep "\n\n" [
@ -30,5 +31,6 @@ with lib;
(builtins.readFile ./branchd.func.sh)
(builtins.readFile ./link_ignored.func.sh)
(builtins.readFile ./branching_setup.func.sh)
(builtins.readFile ./gcpropose.func.sh)
];
}

View file

@ -0,0 +1,177 @@
gcpropose() {
local LITELLM_BASE_URL="http://h001.net.joshuabell.xyz:8094"
local LITELLM_MODEL="azure-gpt-5-mini-2025-08-07"
local mode="staged"
while [ $# -gt 0 ]; do
case "$1" in
-a) mode="all"; shift ;;
-h|--help)
cat <<EOF
Usage: gcpropose [-a]
Propose a short git commit subject line using a LiteLLM model.
Defaults:
- without -a: uses staged diff (git diff --staged)
- with -a : uses full diff vs HEAD (git diff HEAD)
EOF
return 0
;;
*)
echo "Unknown arg: $1" >&2
return 2
;;
esac
done
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Not inside a git repository." >&2
return 1
fi
if ! command -v curl >/dev/null 2>&1; then
echo "Missing dependency: curl" >&2
return 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Missing dependency: jq" >&2
return 1
fi
local diff
if [ "$mode" = "all" ]; then
diff=$(git diff HEAD)
else
diff=$(git diff --staged)
fi
if [ -z "$diff" ]; then
if [ "$mode" = "all" ]; then
echo "No changes vs HEAD." >&2
else
echo "No staged changes." >&2
fi
return 1
fi
local git_status
git_status=$(git status --porcelain=v1 2>/dev/null || true)
local max_chars=10000
diff=$(printf "%s" "$diff" | head -c "$max_chars")
local name_status
if [ "$mode" = "all" ]; then
name_status=$(git diff --name-status HEAD 2>/dev/null || true)
else
name_status=$(git diff --name-status --staged 2>/dev/null || true)
fi
local prompt
prompt=$(cat <<EOF
Propose a concise git commit subject line based on the changes.
Rules:
- Output ONLY the commit subject line.
- Imperative mood.
- Max 72 characters.
- No quotes, no backticks, no trailing period.
git status --porcelain:
${git_status}
files changed:
${name_status}
git diff (truncated):
${diff}
EOF
)
local payload_chat
payload_chat=$(jq -n \
--arg model "$LITELLM_MODEL" \
--arg content "$prompt" \
'{
model: $model,
messages: [
{
role: "system",
content: "You write excellent, conventional git commit subject lines."
},
{
role: "user",
content: $content
}
],
temperature: 0.2
}')
local curl_out http_code body
curl_out=$(curl -sS -w "\n%{http_code}" \
-X POST "${LITELLM_BASE_URL}/v1/chat/completions" \
-H "Content-Type: application/json" \
${LITELLM_API_KEY:+-H "Authorization: Bearer ${LITELLM_API_KEY}"} \
-d "$payload_chat") || return 1
http_code=$(printf "%s" "$curl_out" | tail -n 1)
body=$(printf "%s" "$curl_out" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "LiteLLM request failed (HTTP $http_code)." >&2
printf "%s\n" "$body" >&2
return 1
fi
local message
message=$(printf "%s" "$body" | jq -r '
.choices[0].message.content
| if type == "string" then .
elif type == "array" then (map(select(.type=="text") | .text) | join(""))
else ""
end
' 2>/dev/null || true)
message=$(printf "%s" "$message" | sed -n '1p' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$message" ] && [ "$message" != "null" ]; then
printf "%s\n" "$message"
return 0
fi
local payload_responses
payload_responses=$(jq -n \
--arg model "$LITELLM_MODEL" \
--arg input "$prompt" \
'{
model: $model,
input: $input,
max_output_tokens: 64
}')
curl_out=$(curl -sS -w "\n%{http_code}" \
-X POST "${LITELLM_BASE_URL}/v1/responses" \
-H "Content-Type: application/json" \
${LITELLM_API_KEY:+-H "Authorization: Bearer ${LITELLM_API_KEY}"} \
-d "$payload_responses") || return 1
http_code=$(printf "%s" "$curl_out" | tail -n 1)
body=$(printf "%s" "$curl_out" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "LiteLLM request failed (HTTP $http_code)." >&2
printf "%s\n" "$body" >&2
return 1
fi
message=$(printf "%s" "$body" | jq -r '(.output_text // empty)' 2>/dev/null || true)
message=$(printf "%s" "$message" | sed -n '1p' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$message" ] || [ "$message" = "null" ]; then
echo "Failed to parse model response." >&2
printf "%s\n" "$body" >&2
return 1
fi
printf "%s\n" "$message"
}

58
hosts/oren/flake.lock generated
View file

@ -31,11 +31,11 @@
},
"locked": {
"dir": "flakes/beszel",
"lastModified": 1767572382,
"narHash": "sha256-oDoVrmMpww4uY3Ez1XzrHsxJTZmBMiOO/mNrU2njiWQ=",
"lastModified": 1767575724,
"narHash": "sha256-L+3hoO4t3RCXkp9RXyXpJlCkzj6AdTOsstUv7RphEBM=",
"ref": "refs/heads/master",
"rev": "165f87ebc19a48b56008738d01c5e2e5bebbbdfd",
"revCount": 1038,
"rev": "f86b8085c2ad39986c194b28d51260f8f402572a",
"revCount": 1041,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
},
@ -63,20 +63,14 @@
},
"common": {
"locked": {
"dir": "flakes/common",
"lastModified": 1767572382,
"narHash": "sha256-oDoVrmMpww4uY3Ez1XzrHsxJTZmBMiOO/mNrU2njiWQ=",
"ref": "refs/heads/master",
"rev": "165f87ebc19a48b56008738d01c5e2e5bebbbdfd",
"revCount": 1038,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
"path": "../../flakes/common",
"type": "path"
},
"original": {
"dir": "flakes/common",
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}
"path": "../../flakes/common",
"type": "path"
},
"parent": []
},
"crane": {
"locked": {
@ -123,11 +117,11 @@
},
"locked": {
"dir": "flakes/de_plasma",
"lastModified": 1767572382,
"narHash": "sha256-oDoVrmMpww4uY3Ez1XzrHsxJTZmBMiOO/mNrU2njiWQ=",
"lastModified": 1767575724,
"narHash": "sha256-L+3hoO4t3RCXkp9RXyXpJlCkzj6AdTOsstUv7RphEBM=",
"ref": "refs/heads/master",
"rev": "165f87ebc19a48b56008738d01c5e2e5bebbbdfd",
"revCount": 1038,
"rev": "f86b8085c2ad39986c194b28d51260f8f402572a",
"revCount": 1041,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
},
@ -161,11 +155,11 @@
},
"locked": {
"dir": "flakes/flatpaks",
"lastModified": 1767572382,
"narHash": "sha256-oDoVrmMpww4uY3Ez1XzrHsxJTZmBMiOO/mNrU2njiWQ=",
"lastModified": 1767575724,
"narHash": "sha256-L+3hoO4t3RCXkp9RXyXpJlCkzj6AdTOsstUv7RphEBM=",
"ref": "refs/heads/master",
"rev": "165f87ebc19a48b56008738d01c5e2e5bebbbdfd",
"revCount": 1038,
"rev": "f86b8085c2ad39986c194b28d51260f8f402572a",
"revCount": 1041,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
},
@ -1237,11 +1231,11 @@
},
"locked": {
"dir": "flakes/opencode",
"lastModified": 1767572382,
"narHash": "sha256-oDoVrmMpww4uY3Ez1XzrHsxJTZmBMiOO/mNrU2njiWQ=",
"lastModified": 1767575724,
"narHash": "sha256-L+3hoO4t3RCXkp9RXyXpJlCkzj6AdTOsstUv7RphEBM=",
"ref": "refs/heads/master",
"rev": "165f87ebc19a48b56008738d01c5e2e5bebbbdfd",
"revCount": 1038,
"rev": "f86b8085c2ad39986c194b28d51260f8f402572a",
"revCount": 1041,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
},
@ -1446,11 +1440,11 @@
},
"locked": {
"dir": "flakes/secrets",
"lastModified": 1767572382,
"narHash": "sha256-oDoVrmMpww4uY3Ez1XzrHsxJTZmBMiOO/mNrU2njiWQ=",
"lastModified": 1767575724,
"narHash": "sha256-L+3hoO4t3RCXkp9RXyXpJlCkzj6AdTOsstUv7RphEBM=",
"ref": "refs/heads/master",
"rev": "165f87ebc19a48b56008738d01c5e2e5bebbbdfd",
"revCount": 1038,
"rev": "f86b8085c2ad39986c194b28d51260f8f402572a",
"revCount": 1041,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
},

View file

@ -6,8 +6,8 @@
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
# Use relative to get current version for testin
# common.url = "path:../../flakes/common";
common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common";
common.url = "path:../../flakes/common";
# common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common";
# secrets.url = "path:../../flakes/secrets";
secrets.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/secrets";
# flatpaks.url = "path:../../flakes/flatpaks";