Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [v0.6.3] - 2026-05-21

### Features

- `repo clone <OWNER/NAME> [DIR]` — structured clone command, wraps `gh repo clone` (inherits your `gh auth` for private repos). Optional `--depth N` for shallow clones.
- `repo workspace <OWNER/NAME> [-r REF] [--depth N]` — the higher-level "give me a sandbox" command for agents. Creates a fresh `/tmp/fledge-gh-<owner>-<name>-XXXXXX/<name>` dir, clones the repo there (optionally shallow + at a specific ref), and prints the absolute path on stdout. The `-r REF` is wired through to git's `--branch` so shallow + ref work together.
- `repo workspace-clean <DIR>` — `rm -rf`s a workspace dir. Refuses to remove anything that doesn't live under `/tmp/fledge-gh-`, so a bad agent path can't wipe arbitrary files.

These give agents driven from non-interactive bridges (Discord/Telegram) a structured path to clone a repo for edit work, instead of falling back to `shell-exec git clone ...` which is auto-denied by the Tier 1.5 safety gate in merlin's `--yes` mode.

## [v0.6.2] - 2026-05-21

### Fixes
Expand Down
164 changes: 159 additions & 5 deletions bin/fledge-github-repo
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ REPO=""
REF=""
PATH_ARG=""
JSON=false
CLONE_DIR=""
DEPTH=""

if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then
case "$1" in
Expand All @@ -38,40 +40,88 @@ if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then
shift
fi
;;
clone)
ACTION="clone"
shift
# Two positionals: OWNER/NAME and an optional target DIR.
# `gh repo clone` accepts both; we pass them through verbatim.
if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ] && [[ "$1" == */* ]]; then
REPO="$1"
shift
fi
if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then
CLONE_DIR="$1"
shift
fi
;;
workspace)
ACTION="workspace"
shift
if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ] && [[ "$1" == */* ]]; then
REPO="$1"
shift
fi
;;
workspace-clean)
ACTION="workspace-clean"
shift
if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then
CLONE_DIR="$1"
shift
fi
;;
esac
fi

while [ $# -gt 0 ]; do
case "$1" in
-R|--repo) REPO="${2:-}"; shift 2;;
-r|--ref) REF="${2:-}"; shift 2;;
--depth) DEPTH="${2:-}"; shift 2;;
--json) JSON=true; shift;;
-h|--help)
cat <<'EOF'
fledge github repo — read repo metadata and file contents via the gh CLI

USAGE:
fledge github repo [view] [OWNER/NAME] View repo metadata (default)
fledge github repo file <PATH> Read a file OR list a directory
fledge github repo [view] [OWNER/NAME] View repo metadata (default)
fledge github repo file <PATH> Read a file OR list a directory
fledge github repo clone <OWNER/NAME> [DIR] Clone a repo via `gh repo clone`
fledge github repo workspace <OWNER/NAME> Clone into a fresh /tmp dir, print path
fledge github repo workspace-clean <DIR> rm -rf a workspace dir (safety-checked)

OPTIONS:
-R, --repo <OWNER/NAME> Target repo (default: current working repo).
view also accepts OWNER/NAME as a positional.
-r, --ref <REF> Branch / tag / SHA (file only; default: repo default)
view + clone + workspace also accept it as a positional.
-r, --ref <REF> Branch / tag / SHA (file: lookup; workspace: checkout)
--depth <N> Shallow-clone depth (clone + workspace)
--json Output JSON

The `file` action returns the file's decoded contents when PATH is a
file, or a tab-separated listing (name<TAB>type<TAB>size) when PATH
is a directory — same command, agent can browse and read with one tool.

`clone` wraps `gh repo clone OWNER/NAME [DIR]` directly.

`workspace` is the higher-level "give me a sandbox" command for
agents: creates a fresh `/tmp/fledge-gh-<owner>-<name>-XXXXXX/<name>`
directory, clones the repo there (optionally shallow + at a specific
ref), and prints the absolute path on stdout. Cleanup via
`workspace-clean <DIR>` which `rm -rf`s after sanity-checking the
path is under `/tmp/fledge-gh-`.

EXAMPLES:
fledge github repo
fledge github repo view CorvidLabs/merlin
fledge github repo view -R CorvidLabs/merlin --json
fledge github repo file CHANGELOG.md
fledge github repo file Sources -R CorvidLabs/corvid-verify
fledge github repo file README.md -R CorvidLabs/fledge-plugin-github
fledge github repo file Cargo.toml -r v0.3.0
fledge github repo clone CorvidLabs/corvid-verify
fledge github repo clone CorvidLabs/corvid-verify /tmp/cv --depth 1
fledge github repo workspace CorvidLabs/corvid-verify
fledge github repo workspace CorvidLabs/corvid-verify -r feat/jwks-endpoint --depth 1
fledge github repo workspace-clean /tmp/fledge-gh-CorvidLabs-corvid-verify-abc123
EOF
exit 0
;;
Expand All @@ -87,6 +137,110 @@ if ! command -v gh >/dev/null 2>&1; then
exit 127
fi

if [ "$ACTION" = "clone" ]; then
if [ -z "$REPO" ]; then
echo "fledge github repo clone: missing OWNER/NAME. Try: fledge github repo clone CorvidLabs/merlin" >&2
exit 64
fi
ARGS=("$REPO")
[ -n "$CLONE_DIR" ] && ARGS+=("$CLONE_DIR")
if [ -n "$DEPTH" ]; then
# `gh repo clone` forwards anything after `--` to git, so depth
# has to land there. Position matters: ARGS first, then `--`,
# then git-specific options.
ARGS+=(-- --depth "$DEPTH")
fi
gh repo clone "${ARGS[@]}"
exit 0
fi

if [ "$ACTION" = "workspace" ]; then
if [ -z "$REPO" ]; then
echo "fledge github repo workspace: missing OWNER/NAME. Try: fledge github repo workspace CorvidLabs/merlin" >&2
exit 64
fi

# Sanitize OWNER/NAME for use in a path: replace anything that isn't
# alnum/-/./_ with `_`. That keeps the resulting dir name human-
# readable (so the agent's logs make sense) and avoids the very real
# risk of an attacker-controlled repo name escaping the /tmp prefix
# via something cute like `../../etc/passwd`.
OWNER_NAME_SAFE="$(printf '%s' "$REPO" | tr '/' '-' | tr -c 'A-Za-z0-9._-' '_')"

# The plain repo name (post-slash) becomes the inner clone dir, so
# the agent can `cd $WORKSPACE` and end up at the repo root, matching
# everyone's existing intuition about `git clone owner/repo`.
NAME_ONLY="${REPO#*/}"
NAME_ONLY_SAFE="$(printf '%s' "$NAME_ONLY" | tr -c 'A-Za-z0-9._-' '_')"

# mktemp -d gives us a unique parent. The clone target is one level
# deeper so cleanup of the parent removes both the dir and any junk
# gh / git leave behind.
PARENT="$(mktemp -d "/tmp/fledge-gh-${OWNER_NAME_SAFE}-XXXXXX")"
TARGET="${PARENT}/${NAME_ONLY_SAFE}"

# Build the gh repo clone arg list. Everything after `--` is passed
# to git itself. We put --branch / --depth there together so a
# shallow clone targets the requested ref directly — without that,
# `--depth 1` + post-clone checkout fails because the shallow fetch
# only contains the default branch's tip.
CLONE_ARGS=("$REPO" "$TARGET")
GIT_ARGS=()
[ -n "$REF" ] && GIT_ARGS+=(--branch "$REF")
[ -n "$DEPTH" ] && GIT_ARGS+=(--depth "$DEPTH")
if [ "${#GIT_ARGS[@]}" -gt 0 ]; then
CLONE_ARGS+=(-- "${GIT_ARGS[@]}")
fi

# Clone with stderr passed through so the user/agent sees progress
# and any auth errors. Stdout is reserved for the final path so a
# JSON-parsing caller doesn't get progress noise mixed in.
if ! gh repo clone "${CLONE_ARGS[@]}" >&2; then
# Best-effort cleanup of the empty parent; failure is fine.
rmdir "$PARENT" 2>/dev/null || true
echo "fledge github repo workspace: gh repo clone failed" >&2
exit 1
fi

if [ "$JSON" = "true" ]; then
printf '{"path":"%s","repo":"%s","ref":"%s"}\n' "$TARGET" "$REPO" "${REF:-}"
else
printf '%s\n' "$TARGET"
fi
exit 0
fi

if [ "$ACTION" = "workspace-clean" ]; then
if [ -z "$CLONE_DIR" ]; then
echo "fledge github repo workspace-clean: missing DIR. Try: fledge github repo workspace-clean /tmp/fledge-gh-XXXX" >&2
exit 64
fi
# Safety: only delete dirs that look like fledge-gh workspaces. A
# bug in the agent's reasoning shouldn't be able to wipe arbitrary
# paths via this tool. The prefix check is intentionally strict —
# symlinks and `..` segments fail it cleanly.
case "$CLONE_DIR" in
/tmp/fledge-gh-*) ;;
*)
echo "fledge github repo workspace-clean: refusing to remove '$CLONE_DIR' (must start with /tmp/fledge-gh-)" >&2
exit 64
;;
esac
if [ ! -d "$CLONE_DIR" ]; then
echo "fledge github repo workspace-clean: no such directory: $CLONE_DIR" >&2
exit 1
fi
# If the caller passed the inner clone path, we still want to remove
# the parent so we don't leave an empty fledge-gh-* shell behind.
# Walk up only when CLONE_DIR's parent itself matches our prefix.
PARENT_OF="$(dirname "$CLONE_DIR")"
case "$PARENT_OF" in
/tmp/fledge-gh-*) rm -rf -- "$PARENT_OF";;
*) rm -rf -- "$CLONE_DIR";;
esac
exit 0
fi

if [ "$ACTION" = "view" ]; then
# ${ARRAY[@]+...} expansion: with `set -u`, "${EMPTY[@]}" errors on
# bash < 4.4. Use the +-form so the expansion only happens when the
Expand Down
6 changes: 3 additions & 3 deletions plugin.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[plugin]
name = "fledge-plugin-github"
version = "0.6.2"
version = "0.6.3"
description = "GitHub commands for fledge — list/view/create/comment/review/merge PRs, list/view/create/comment/close issues, read repo files, view CI checks, and poll for daemon events via the gh CLI"
author = "0xLeif"
license = "MIT"

[[commands]]
name = "github"
description = "GitHub commands via the gh CLI. All subcommands accept -R OWNER/NAME to target any repo (the agent's working dir is otherwise used). Subcommands: prs (list/view <num> [--diff --comments]/create/comment <num> -b BODY/review <num> --approve|--request-changes|--comment-review -b BODY/merge <num>/close <num>/reopen <num>), issues (list/view <num> [--comments]/create/comment <num> -b BODY/close <num>/reopen <num>), repo (view / file <path> [--ref REF]), checks (CI status), poll (daemon event polling)."
description = "GitHub commands via the gh CLI. All subcommands accept -R OWNER/NAME to target any repo (the agent's working dir is otherwise used). Subcommands: prs (list/view <num> [--diff --comments]/create/comment <num> -b BODY/review <num> --approve|--request-changes|--comment-review -b BODY/merge <num>/close <num>/reopen <num>), issues (list/view <num> [--comments]/create/comment <num> -b BODY/close <num>/reopen <num>), repo (view OWNER/NAME / file <path> [--ref REF] (handles files and dirs) / clone OWNER/NAME [DIR] / workspace OWNER/NAME [--ref REF] [--depth N] (clones into /tmp/fledge-gh-* and prints the path — preferred for agents needing a local edit sandbox) / workspace-clean DIR), checks (CI status), poll (daemon event polling)."
binary = "bin/fledge-github"
args = [
{ name = "args", type = "string", required = true, description = "Subcommand and options. Pass -R OWNER/NAME to target any repo without changing directory. Examples: 'prs view 5 -R CorvidLabs/corvid-verify --diff', 'prs comment 208 -b \"LGTM\"', 'issues create -R CorvidLabs/merlin -t \"Bug\" -b \"...\"', 'repo file CHANGELOG.md -R CorvidLabs/merlin'" },
{ name = "args", type = "string", required = true, description = "Subcommand and options. Pass -R OWNER/NAME for cross-repo lookups. For agent edit sandboxes use 'repo workspace OWNER/NAME [-r BRANCH] [--depth 1]' — it returns a /tmp path; clean up with 'repo workspace-clean <DIR>'. Examples: 'prs view 5 -R CorvidLabs/corvid-verify --diff', 'repo file Sources -R CorvidLabs/corvid-verify --ref feat/x', 'repo workspace CorvidLabs/corvid-verify -r feat/jwks-endpoint --depth 1'" },
]
Loading