@ecology91/glab-axi
v0.7.1
Published
AXI-compliant glab CLI wrapper for GitLab — token-efficient TOON output, contextual suggestions, idempotent mutations
Readme
glab-axi
An AXI-compliant CLI that wraps the GitLab glab CLI for autonomous agents.
It is the GitLab twin of gh-axi: token-efficient TOON output, minimal default schemas, contextual next-step suggestions, idempotent mutations, and structured errors on stdout - everything an agent needs to operate GitLab from the shell without burning tokens or guessing.
Why
Agents drive CLIs by reading stdout. Raw glab/REST output is verbose JSON full of fields an agent will never use, mutations error on already-satisfied state, and failures leak stack traces. glab-axi fixes all of that:
- TOON, not JSON - ~40% fewer tokens, still readable.
- Minimal schemas - lists default to 3-5 fields; ask for more with
--fields a,b,c. - Pre-computed aggregates - pipeline views report
checks: N passed, M failed, K runningand an at-a-glance verdict, so an agent never has to count jobs. - Stuck pipelines are named, not just slow - a pending job no active runner can take is reported in a
stucksection with the reason, so a poller can tell "blocked, escalate" from "still running, keep waiting". - Real totals, not guesses - list output reads GitLab's own count (
count: 30 of 847 total) instead of restating the--limitit was just given. - Bounded, greppable CI logs -
ci logstrips ANSI noise and truncates to a token-safe tail; a truncated trace also spills the full log to a local file the agent can grep instead of paying for it in context. - Idempotent mutations - closing a closed issue or merging a merged MR is a no-op with exit 0.
- Definitive empty states and contextual
help[]suggestions on every list and mutation. - Structured errors on stdout - actionable, and they never leak the underlying tool's name.
- Fails loud on a typo - an unrecognized flag or subcommand exits 2 naming what was wrong and listing the valid set, rather than being dropped. A silently ignored
--stat closedwould hand back open issues at exit 0, and an agent cannot tell that from the filtered result it asked for.
Quick Start
Install the Agent Skill globally from the root of an authenticated checkout:
npx skills add . --skill glab-axi -gYou can also install directly from the private self-hosted GitLab project:
npx skills add https://gitlab.example.com/group/glab-axi.git --skill glab-axi -gCopy your private project's clone URL and keep its .git suffix. Configure Git credentials for that host before running the command: the installer disables credential prompts while cloning, so it cannot stop to ask for a username or password.
The flags keep the install narrow and guided:
-gselects global scope, making the skill available across projects.--skill glab-axiinstalls only the user skill and excludes the maintainer-onlyglab-axi-releaseskill.- Leaving off the installer's
-ykeeps the human agent, installation-method (when applicable), and final confirmation prompts. When the installer detects that an AI agent is running it becomes non-interactive automatically, so those prompts will not appear in an agent-driven shell.
The installed Agent Skill invokes the exact released @ecology91/[email protected] package on demand, while commands and follow-up suggestions keep using the glab-axi executable name. It does not depend on a global npm CLI install.
You still need the GitLab glab CLI installed and authenticated (glab auth login), and Node.js 20 or newer.
For a self-hosted instance, authenticate glab against that host. Use -R <host>/group/project for a project target, --host <host> for host-level operations, or GITLAB_HOST=<host> to select the host for a project already resolved from a hostless -R or the current GitLab remote (see Targeting a project).
The skill is not a user-facing slash command (user-invocable: false).
Just ask for anything that touches GitLab - filing an issue, reviewing a merge request, chasing a failed pipeline, cutting a release - and the agent loads the skill on its own when it recognizes the task.
It loads on demand rather than sitting in the context window, so it costs no per-session tokens, and it works in any agent that supports the skill format.
Other ways to install
The scoped CLI can also run without a global install, and it can add ambient session context when you opt in.
Zero setup
Run a specific published version without a global install:
npx -y @ecology91/[email protected] mr --helpGlobal npm install
A global install gives you the glab-axi command directly, which is handy for running it yourself:
npm install -g @ecology91/glab-axi
glab-axi issue listThe generated skill's versioned scoped request does not depend on the global install. The bare command stays on the globally installed version until you upgrade it.
Compare the installed command against what is published:
glab-axi --version
npm view @ecology91/glab-axi versionIf those differ, upgrade the global copy in place:
npm install -g @ecology91/glab-axi@latestOr, if you only use the skill and no session hook needs the bare glab-axi command, drop the global copy:
npm uninstall -g @ecology91/glab-axiSession hook
Want ambient GitLab context - the current project's open issues and merge requests - in every agent session, instead of loading on demand? With the CLI installed globally, opt into the hook:
glab-axi setup hooksThat installs an idempotent SessionStart hook for Claude Code, Codex, and OpenCode.
Each session then opens with a compact dashboard of the current project, so the agent can act immediately with no invocation needed.
Restart your agent session afterwards so the new hook takes effect; re-running the command repairs the hook's path after a reinstall.
Usage
Run with no arguments for a dashboard of the current project (open issues, open merge requests, suggested next commands):
glab-axiIf no GitLab project resolves, it prints project: none with a hint instead of guessing. If a request to the server fails, the affected section renders unavailable - <reason> rather than a false 0 open - a real zero and "could not ask the server" are different facts.
Drill in command-first:
glab-axi issue list --state opened
glab-axi issue view 12 --comments
glab-axi issue links 12
glab-axi mr view 42 --full
glab-axi mr view 42 --reviews
glab-axi mr diff 42
glab-axi ci status --branch main
glab-axi ci log 46450 --fullEvery response ends with help: hints for logical next steps. Run glab-axi --help for global flags, or glab-axi <command> --help for per-command usage.
Commands
| Command | What it does |
|-----------|--------------|
| (none) | Dashboard of the current project |
| issue | list / view / links / create / edit / close / reopen / comment |
| mr | list / view / create / update / merge / approve / unapprove / checks / diff / comment (by IID; view, merge, checks, and diff also take a full MR URL) |
| ci | list / view / status / jobs / watch / log / run / retry / cancel (pipelines; watch blocks until a pipeline finishes and exits non-zero if it did not succeed) |
| project | view / list / create / delete (delete names its target and requires --yes) |
| repo | create-file / create-branch (writes the project's git contents) |
| label | list / create / edit / delete |
| variable| list / get / set / delete (plain, unmasked CI/CD variables) |
| secret | list / set / delete (masked & protected CI/CD variables; list never reveals values) |
| release | list / view / create / edit / delete |
| search | issues / mrs / projects |
| api | raw GitLab REST passthrough with a {project} placeholder |
| auth | status / git-credential (host-scoped credentials and install introspection - see Credentials) |
| config | get (read-only configuration introspection; refuses credential keys) |
| setup | install agent SessionStart hooks |
Issues and merge requests are addressed by their project-scoped IID (the number in the URL).
Targeting a project
glab-axi is fully generic - it works against gitlab.com or any self-hosted GitLab. The target project is resolved in priority order:
-R [host/]group/projectplaced after the command (e.g.glab-axi mr list -R gitlab.example.com/group/project). A two-segment value is alwaysgroup/project, even when the group name contains a dot (e.g.firstname.lastname, the standard username shape on LDAP/SSO instances); only a 3+-segment value can lead with a host, and then only when it's a hostglabis already configured for or (as a last resort) contains a dot. Nested group paths are supported.- The
origingit remote of the current repository.
A git remote only resolves to a project when its host is one the glab CLI is actually configured for, or when GITLAB_HOST explicitly names that host. A remote on a different forge (GitHub, Bitbucket, etc.) resolves to no project rather than a guess.
The HOST follows explicit identity rather than ambient precedence. A host carried by -R cannot be redirected by GITLAB_HOST; --host and a host carried by -R must agree or the command refuses. When -R carries no host, --host wins over GITLAB_HOST, which may otherwise override the git remote. --host alone (no -R) targets a self-hosted instance for host-level operations that have no project - search projects, project list, api user - without a project in scope; a project-scoped command still fails loud if no project resolved. A host-only -R <host> (naming a host but no group/project) is rejected with a VALIDATION_ERROR pointing at --host, rather than silently falling through to the default host.
mr view, mr merge, mr checks, and mr diff accept a full merge request URL in place of the IID (e.g. glab-axi mr merge https://gitlab.example.com/group/subgroup/project/-/merge_requests/42). The URL's host, full nested project path, and IID are one identity and override ambient remote or GITLAB_HOST state. Explicit -R or --host values must match that URL; contradictory identities are rejected instead of resolved by precedence.
mr merge --sha <40-hex> binds an immediate merge to the reviewed source head. It checks the current head, still sends GitLab's server-side sha compare-and-swap to close the race, and distinguishes a fresh guarded merge, already-merged exact head, stale head, already-merged different head, and contradictory post-merge response. --sha cannot combine with --rebase (which creates a new head) or --auto (which defers the merge beyond the reviewed instant).
If GitLab reports a successful guarded merge but omits the source-head fields, the CLI performs one canonical MR read. A matching merged head returns the ordinary verified receipt with source_head_sha and head_match: true. If independent head evidence remains unavailable, the mutation still returns success with state: merged, the requested reviewed_head_sha, and head_verification: unavailable; it deliberately omits source_head_sha and head_match. Callers must treat that as “merge completed, independent head receipt unavailable,” not as a verified head match. The already-merged preflight remains stricter and refuses success unless GitLab records the exact reviewed head.
mr create accepts native --source-branch/--target-branch and the --head/--base aliases. For a same-host cross-project MR, an explicit -R identifies the target and --source-project <id|[host/]group/project> identifies the source (including an existing fork). The command resolves and verifies both projects on one GitLab host, checks for an already-open MR with the exact project IDs and branches, then POSTs through the source project with the target's numeric ID. A repeat returns already: true rather than creating a duplicate, and the create receipt must report the resolved project IDs. It creates a merge request between existing projects; it does not create a fork.
mr view --reviews adds approval state (who approved, approvals given/required) and discussion-thread resolution counts. GitLab review semantics are approvals, unapproval, comments, and resolving discussions; there is no direct GitHub requested-changes equivalent, so this CLI deliberately does not emulate an mr review verdict. mr diff prints a bounded per-file summary (path, status, +/- line counts) by default; --full emits the complete reconstructed unified diff. mr merge --auto sets GitLab's merge-when-pipeline-succeeds: it merges immediately if there is no pipeline (or it already passed), otherwise it defers and reports the scheduled state instead of a merge commit SHA; it cannot combine with --rebase. When GitLab refuses a merge, the error names the specific cause plus the command that clears it. mr list and mr view accept the same --jq/--json escape hatches as api (see below).
# explicit host + project
glab-axi issue list -R gitlab.example.com/group/subgroup/project
# project from the git remote, host overridden by env
GITLAB_HOST=gitlab.example.com glab-axi mr list
# host-level op on a self-hosted instance, no project involved
glab-axi search projects backend --host gitlab.example.comProjects and their repositories
project addresses the project entity; repo writes its git contents.
Together they cover a project's whole lifecycle without dropping to api.
# create a project, seed its default branch, and open a feature branch with a diff
glab-axi project create my-group/my-service --readme
glab-axi repo create-file .gitlab-ci.yml -R my-group/my-service --content-file ci.yml
glab-axi repo create-branch feature-x -R my-group/my-service
glab-axi repo create-file src/app.ts -R my-group/my-service --branch feature-x --content "export const app = 1;"
# tear it down again
glab-axi project delete my-group/my-service --yesrepo create-file commits a single file directly to --branch (defaulting to the project's default branch), creating that branch when the repository is still empty.
Content comes from --content, --content-file, or piped stdin.
repo create-branch branches from --ref (also defaulting to the default branch).
Both are idempotent: an existing file or branch is a no-op (already: true), never an overwrite.
repo create-file writes UTF-8 text only; binary content (images, compiled assets) is rejected with an actionable error rather than silently corrupted.
project delete is destructive, so it takes its target as an explicit positional (a numeric project id, or a [host/]group/project path) rather than falling back to the resolved project, and it requires --yes - it never prompts.
Deleting an already-absent project is a no-op (already_absent: true).
The reported outcome is read back from the server, not assumed: on instances with delayed project deletion enabled the project isn't purged immediately, it's renamed and marked for deletion, so the response reports status: scheduled plus purge_after instead of status: ok (deleting an already-scheduled project again is also a no-op, already: true).
GitLab project creation has no direct equivalent to GitHub's clone/template flags in this wrapper. project create --clone and --template remain explicit validation refusals rather than silently claiming unsupported behavior.
Raw API passthrough
Anything the dedicated commands do not cover, reach via api. The {project} placeholder is replaced with the resolved, URL-encoded project id:
glab-axi api projects/{project}/members
glab-axi api POST projects/{project}/labels --raw-field name=urgent --raw-field color=#d9534f
glab-axi api projects/{project}/pipelines --paginateBy default api emits TOON with noisy fields stripped. To pull a single field or feed the response to your own tooling, use --jq or --raw, which both operate on the raw, unmodified JSON:
glab-axi api projects/{project}/merge_requests/5 --jq .state # -> opened
glab-axi api projects/{project}/merge_requests/5 --jq .sha # head SHA
glab-axi api projects/{project} --raw | jq .default_branch--jq <expr> applies a jq expression (raw output, like jq -r) and needs the jq binary on PATH; --raw (alias --json) prints the JSON response verbatim and needs nothing. When both are passed, --jq wins.
mr list and mr view expose the same --jq/--json flags, operating on GitLab's raw response and bypassing schema flags like --full/--comments/--reviews:
glab-axi mr view 42 --jq .detailed_merge_status
glab-axi mr view 42 --jq '{sha: (.sha // .diff_refs.head_sha), target_branch, source_project_id, target_project_id, state, detailed_merge_status, merge_commit_sha, squash_commit_sha}'
glab-axi mr list --state opened --jq '.[].iid'The mr view --json response is GitLab's raw MR object. The scalar fields in the example are the stable pre/post evidence surface: current source head, target branch, source and target project IDs, state, detailed merge status, and merge/squash commit SHAs. Callers must require scalar output and validate any commit value as exactly 40 hexadecimal characters. --jq uses the external jq binary; use --json and parse the raw JSON yourself when jq is unavailable.
Credentials
Cloning or pushing a private project over HTTPS needs a credential.
auth reads the one the GitLab CLI already manages, so nothing has to parse its config file - an internal detail that can change without warning.
A credential is host-scoped, so auth is host-addressed like the other host-level operations: pass --host <host>, or let a -R host/group/project or the git remote supply it.
auth status answers whether a working credential exists, and never prints it:
glab-axi auth statusauth status is diagnostic and intentionally exits successfully even when it reports an absent or unavailable credential. For a readiness gate that must fail when authentication does not work, use an authenticated read: glab-axi api user --host <host>.
install:
bin: /usr/bin/glab
version: 1.53.0
config_file: ~/.config/glab-cli/config.yml
default_host: gitlab.com
hosts[2]{host,token,account}:
gitlab.com,absent,no credential
gitlab.example.com,present,someuser
help[3]:
Default host gitlab.com has no working credential, so any command omitting --host targets it - pass `--host <host>` explicitly, or change the default
...account is who the credential actually authenticated as, not merely what is on disk - a stale token reports unavailable - <reason> rather than a misleading green.
token reports presence only, never any part of a value.
Pass --host <host> to scope the report to one host; without it, every configured host is reported.
Diagnosing setup
The install block answers a question nothing else on the agent surface can: which binary is this actually driving, and which configuration is it reading.
That matters because the answer is not always the one you expect.
A machine with two installs of the GitLab CLI on PATH - say an OS package early and a much newer snap late - silently drives whichever comes first, with its own config file and its own default host.
When more than one is found, a shadowed section names them all:
shadowed[2]{path,version,active}:
/usr/bin/glab,1.36.0,yes
/snap/bin/glab,1.108.0,noThe section only appears when there is a genuine conflict, so seeing it at all is the finding.
One binary reachable through several PATH entries (/usr/bin and /bin on a merged-usr system) is one install, not a conflict, and is not reported as one.
default_host is the other half.
It is the host a command that omits --host lands on, so a default pointing at an instance you never authenticated against means a single call that forgets the flag aims at a dead account.
When that host has no working credential, auth status says so in help[] rather than leaving it to be inferred.
auth git-credential is a git credential helper: it speaks git's key=value protocol on stdin and stdout, so it can be handed straight to git.
git -c credential.helper='!glab-axi auth git-credential' \
clone https://gitlab.example.com/group/project.gitTwo things to know about it:
- It is the only surface that emits a password, because that is what the git protocol requires. It is meant for git to consume, not for an agent to read, and whatever invokes it owns keeping that output out of logs, transcripts, and status lines. Use
auth statuswhenever the question is "do I have a credential" rather than "give me the credential". - It does not emit TOON, and stays silent on failure. git parses its stdout as credential fields and treats anything else as malformed, so a structured error would break the operation the verb exists to serve. The exit code carries the outcome;
auth statusis where a readable diagnosis lives.
Reading is all it does - store and erase pass through untouched, and nothing here writes, rotates, or caches a credential.
Note that GITLAB_TOKEN, when set, answers for every host and overrides the per-host store, so a credential reported under one host may be that environment token rather than an entry for that host.
Configuration
config get reads the GitLab CLI's own configuration, reporting which scope answered:
glab-axi config get host # global
glab-axi config get api_host --host gitlab.example.com # per-hostconfig:
key: host
value: gitlab.com
scope: globalA key that is genuinely unset reads value: unset; a read that failed errors instead, because "unset" and "could not read" are opposite facts.
Credential keys are refused. Any key whose name contains token is rejected before the read happens - the underlying CLI will happily print a live token, and this one never does.
Use auth status for the question that actually needs answering: whether a credential is present and still works.
config is read-only; config set and config list are deliberately refused, the latter because a bulk dump would carry the per-host token to stdout.
Development
npm install
npm run dev -- issue list # run from source via tsx
npm run build # tsc -> dist/
npm test # vitest (unit tests mock the glab layer; no network)
npm run lint
npm run format:check
npm run skill:check # fail if SKILL.md is staleThe skill shipped in the npm package is generated from the CLI's own help (npm run skill:build), and CI fails if it has drifted - commit the regenerated file.
Architecture notes live in AGENTS.md. The short version: every shell-out goes through src/gl.ts, which targets GitLab via glab api (REST passthrough) - the host through GITLAB_HOST, the project through its URL-encoded path. src/commands/mr.ts is the reference template for the per-domain command files.
Releasing
Every notable change is recorded under ## [Unreleased] in CHANGELOG.md, following the Keep a Changelog convention.
This managed fork publishes the public scoped package @ecology91/glab-axi from a reviewed, clean commit. The executable remains glab-axi. The initial fork release is 0.7.0, derived from upstream glab-axi v0.6.0 (102871a) plus the capability and package-identity changes recorded in this changelog.
Publishing is an explicit maintainer action, not a GitHub release side effect. Reverify npm whoami, confirm npm view @ecology91/glab-axi version is absent or older, run the full validation and packed-tarball checks, commit and push the release, then publish with npm publish --access public from that verified commit. Finally install into a clean temporary prefix and prove both glab-axi --version and glab-axi mr --help match the published contract.
The complete command-by-command procedure lives in skills/glab-axi-release/SKILL.md. It is the release source of truth.
License
MIT (c) Christopher McKay
