Tool Reference¶
The server has two surfaces: HTTP endpoints for session lifecycle and diff upload, and MCP tools for the reviews themselves. Your client calls both for you — this page is the reference for building against the server directly, or for adding a client CloudAEye does not yet ship a guide for.
The base URL and the MCP transport URL are the same string, e.g. https://api.cloudaeye.com/mcp.
HTTP endpoints¶
Both endpoints require a CloudAEye product API key in an X-Product-API-Key header, and the tenant_key the key belongs to. The key must carry that tenant on its record and grant the Code Review product; a mismatch answers 403.
POST /session¶
Mint or resume a session. A session is identified by (tenant, repository, developer) — not by branch or head. One developer working on one repository has exactly one session, and it survives branch switches and commits. The client sends no session id and holds no state; the server resolves the same session every time.
Request body (JSON):
| Field | Required | Description |
|---|---|---|
repo |
yes | Repository name |
branch |
yes | Current branch. Refreshed on every resume — state, not identity |
head |
yes | Current git rev-parse HEAD. Also refreshed on resume; it is what the context refresh fetches base file contents against |
tenant_key |
yes | Your CloudAEye tenant. Selects the database holding the repo integration, the code-context graph and the Jira installation |
user_name |
no | Scopes the session, so two developers on one repository don't share one |
pr_number |
no | Review an open pull request of this repository instead of the working tree. The server fetches the diff itself, so no upload step follows. Part of the session's identity, so a PR session never overwrites your working-tree one |
language |
no | Legacy override. Leave it out — the server derives it from the diff. |
Response:
{
"session_id": "…",
"resumed": false,
"target_branch": "main",
"repo_full": "owner/name"
}
target_branch and repo_full come from your tenant's integration record — the bare repo name you sent is resolved into owner/name, and target_branch is the review baseline.
When the repository is not integrated, the response carries target_branch: "" plus target_branch_error and an integration_url naming where to connect it. This is not an error: the session still works, and the client falls back to diffing against local HEAD.
On a resume where a task was previously checked, the response also includes a prior_task block (task_description, task_source) so a client can offer to reuse it instead of re-prompting.
With pr_number set, the response carries a pull_request block — number, title, draft, base, head, head_sha, changed_files — and the diff is already staged, so skip POST /upload. The pull request must be open (drafts count), merge into your integrated branch, not come from a fork, and change at most 50 files; anything else answers status: error with a message naming which rule it broke. Every tool below works on a PR session — inspect_diff, slop_score, control_flow, review_arch, describe_change, ask and check_task — and control_flow and review_arch are the two that can post back to it. change_risk works only on a PR session, and can post back to it too. Fix planning (/cloudaeye:implement) does not: its plans describe edits to a working tree a PR session has none of.
Errors: 400 if the body is not JSON or a required field is missing; 401 for a missing, unknown or expired key; 403 if the key does not belong to the named tenant or lacks the Code Review product; 503 if the server cannot reach its database.
POST /upload/{session_id}¶
Replace the diff for an existing session. Multipart, two fields: file (the diff) and base_sha (the commit it was taken against — the server stages file contents against it).
curl -s -F "file=@session.diff" -F "base_sha=$BASE" \
-H "X-Product-API-Key: $CLOUDAEYE_API_KEY" \
https://api.cloudaeye.com/upload/$SESSION_ID
Response:
{ "session_id": "…", "diff_unchanged": false }
diff_unchanged: truewhen the uploaded bytes are identical to the last upload and the cached context is still on disk — the server skips the cache wipe.- Otherwise the cached code-context graph and staged post-edit file tree are invalidated, so the next tool call re-derives them against the new diff.
Errors: 404 if the session is unknown — call POST /session first; 400 if the file field is missing.
The diff is never passed inline to an MCP tool. It lives on the server keyed by
session_id. Always derive it fromgit diffrather than constructing it by hand — see Deriving the diff.
MCP tools¶
The tools below cover the review flows. All return a JSON string. The session tools take a session_id and reference the uploaded diff by it; they are not key-authenticated, since a session_id is only obtainable by a caller who authenticated to POST /session. Three tools — check_pr, add_docs, add_tests — take no session at all: they name a repository and a pull request, and the hosted service fetches everything itself. They are described at the end.
inspect_diff¶
One tool covers all three review passes. profile decides which checks run.
inspect_diff(session_id: str, intent: str = "", profile: str = "inspect",
context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session |
intent |
Optional. A one-line note on what the agent just changed — steers what the planner prioritises, and on a recheck says which findings were already addressed |
profile |
inspect (default), security, or review. An unknown value falls back to inspect, never to a wider set |
context |
Optional per-call extras: pr_title, pr_description, review_config, report_types |
profile |
Report types |
|---|---|
inspect |
BUG_REPORT — no security checks |
security |
SECURITY_REPORT, LLM_SECURITY_REPORT, AIAGENT_SECURITY_REPORT, MCP_REPORT, SECRET_REPORT |
review |
all of the above |
Returns: verdict, findings[], profile, session_id, and counts when there are findings.
Everything else appears only when something didn't do what you'd assume — degraded, context_refresh, secret_scan, compiler_check, cached. No field means it went fine.
slop_score¶
Scores the session's diff for signs of low-quality code and returns a rendered card.
slop_score(session_id: str, run_review: bool = True,
context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session — a working-tree session or a pull-request one |
run_review |
Leave true. The score is arithmetic over review findings, so the review has to have run on this diff; false scores only what is already stored, which on a fresh session is every signal unmeasured |
context |
Optional per-call extras: scope_path, the path the diff was narrowed to. It filters nothing server-side — you uploaded an already-narrowed diff — but it is what makes the response say what it covered |
Nine signals — duplicate code, hallucinated API, dead code, missing tests, high complexity,
generic naming, boilerplate comments, overengineering, style inconsistency — scored out of
100. Whatever inspect_diff already worked out on the same diff is reused rather than
recomputed.
Returns:
| Field | Meaning |
|---|---|
card |
The rendered card. Print it verbatim — the rules about what may be shown are part of the output: an unmeasured signal renders as a dash and a reason rather than a bar at zero, and a partially covered signal says so on its own row |
total |
The score out of 100, as the card prints it. Not calibrated yet, and the card says so |
band |
One of the four review bands |
signals |
Each signal's finding count; null for a signal nothing measured |
kinds |
Findings by category, so a caller can say 2 missing tests · 1 dead import without reading the card |
unmeasured |
Every signal nothing looked at, with the reason. Never trimmed, and never to be reported as clean |
headline |
The size and shape of the change — lines, files, and how many changed functions carry a behavioural edit |
band_source, band_reason and arithmetic_band appear only when the band was moved
after the evidence was weighed. scope appears only when the diff was narrowed, and
degraded only when the review underneath could not read your post-edit source — which
makes the whole card unusable, not merely low. When nothing at all could be measured
there is no band, and a reason field says why.
There is deliberately no min_severity. A floor drops findings before they are
counted, so every signal's numerator would shrink while its denominator stayed put, and
the score would fall because less was reported rather than because less was wrong.
How the score is built — each signal's weight and the points it earned — is not part of the result or the card.
control_flow¶
Draws what the session's diff did to the flow of control, as before/after sequence diagrams, and returns a rendered card.
control_flow(session_id: str, depth: int = 2, label: bool = True,
cross_repo: bool = True, post: bool = False,
context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session — a working-tree session or a pull-request one |
depth |
Call hops to follow from the flow's root. 2 is the readable default; 3 is usually past what a reader can hold |
label |
Make the one model call — it chooses which flows to draw, names them, and writes the opening sentence. false draws the highest-reaching flow with its raw symbol name and costs nothing |
cross_repo |
Also search the tenant's other connected repositories for callers this change may have broken. One confirm call plus a graph read per sibling. Never silently skipped when true |
post |
Put the card on the pull request as a comment through the CloudAEye App. Writes where other people can see it, so off by default; needs a session opened with pr_number, and a working-tree session is refused rather than ignored |
context |
Optional per-call extras: scope_path, the path the diff was narrowed to. Filters nothing server-side — you uploaded an already-narrowed diff — but is what makes the response say what it covered |
The arrows are read off the repository's stored code graph, where every call site carries a line, so they are resolved facts rather than a reconstruction from the patch. Exactly one model call is made, and it never adds, removes or reorders a message: it chooses among finished pictures, names them, and summarises. Its failure leaves the diagram unchanged and the opening line written from the counts.
Returns:
| Field | Meaning |
|---|---|
card |
The rendered card. Print it verbatim, every mermaid fence included — it is the same block that gets posted on the pull request, and the rules about what it may show are part of the output: the opening line, the facts table, the before/after pair, the marked arrows |
changed |
Counts of added, removed and retargeted messages across both pictures |
messages |
Messages on the after picture |
flow |
The flow drawn — a route label like POST /api/v1/orders when the root is a handler, otherwise the symbol name |
diagrams |
2 when the card holds a Before and an After. Absent when it holds one |
flows / drawn |
How many independent flows the change has, and how many the card drew. drawn: 0 is a refusal the card explains |
unresolved |
Calls the graph could not follow — drawn as dashed arrows, counted here, never listed |
pr_number / pr_url |
Only on a pull-request session. pr_url is constructed, never read from a payload |
posted |
Only with post: true. status is ok when the card is on the pull request; anything else means it is not, and says why |
diff_measured: false rides the result when there was no pre-edit graph to compare
against — every marker was suppressed, and that is the opposite claim from "nothing
changed". cross_repo appears with affected, repos and searched when callers were
found in other repositories, and with unmeasured (and not_indexed) when the search
could not run or could not see every sibling — never as a silent absence. scope appears
only when the diff was narrowed, and context_refresh only when the graph was not
refreshed with this diff.
unavailable with a note is an ordinary answer: no code graph, no changed symbol the
graph carries, or a change whose symbols make no resolvable calls. It is not an error to
retry.
review_arch¶
Reviews what the session's diff did to the repository's architecture, and returns a rendered card.
review_arch(session_id: str, post: bool = False,
context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session — a working-tree session or a pull-request one |
post |
Put the card on the pull request as a comment through the CloudAEye App. Writes where other people can see it, so off by default; needs a session opened with pr_number |
context |
Optional per-call extras: pr_title on a working-tree session (a pull-request session carries its own), and scope_path when the diff was narrowed to a path |
The layers and components come from the repository's architecture model, and the edges are read out of its code graph before and after the change, so every dependency on the card is a resolved fact. Two model calls are made — one confirms convention candidates, where no verdict drops the candidate; one writes the walk-through and answers the questions, where no answer leaves the measured card with its questions printed as questions — and neither can add a file, a layer or an edge.
Returns:
| Field | Meaning |
|---|---|
card |
The rendered card. Print it verbatim, every mermaid fence included — the system, the repository and the change pictures, the numbered findings, Across the tenant, the questions, and a footer that counts pre-existing violations and names the model the change was graded against |
verdict |
PASS, FINDINGS or FAIL. FAIL needs a layer the repository declares on both ends of a violation; an inferred layer never fails a change |
findings / by_kind |
How many findings, and how many of each kind |
layers_touched / components_touched / deployables_touched |
What the change reached |
pre_existing |
Violations already in the touched files — counted, never listed. A clean result over a large number is a clean change in a repository already in violation |
questions |
How many questions the card asks |
model |
source (stored or built from this session's graph), labelled (false means components are named by directory), and labelling (started or in_progress while they are being named in the background) |
changed_files |
How many changed files the review read |
pr_number / pr_url |
Only on a pull-request session |
posted |
Only with post: true. status is ok when the card is on the pull request; anything else means it is not, and says why |
Each of these appears only when it applies:
- diff_measured: false: there was no pre-edit graph, so no edge is marked added and cycles and direction were not compared. That is the opposite claim from "nothing changed".
- confirm_failed / explain_failed: the convention candidates were dropped, or the questions went unanswered. Neither is a clean result.
- system_map counts the services read from the tenant's deployment files, with measured: false and a reason when the system picture could not be drawn.
- service_map counts the connected repositories found calling this one.
- cross_repo counts breaks in other repositories.
The last two carry measured: false when they did not run, never a silent absence.
unavailable with a note is an ordinary answer: no code graph, no architecture model that
could be loaded or built, or a change with no source code in it. That last one says what the
change touched instead (touched: manifests, deployment files, CI, docs), and
unread_languages names changed code in a language the review does not read yet — a gap in
what was reviewed, not a clean result.
change_risk¶
Whether a pull request needs a careful human review, why, and who to ask: a risk score from who wrote it, what it touches and what those files and that author have been through over a window of weeks, together with the pull request's own slop score.
change_risk(session_id: str, days: int = 21, post: bool = False,
context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session, opened with pr_number. A working-tree session is refused: the history is the pull request's repository's |
days |
The window, 7 to 90. Every trend is one value a week inside it |
post |
Put the card on the pull request, editing CloudAEye's earlier risk card there rather than adding a second. Writes where other people can see it, so off by default |
context |
Optional per-call extras |
It is queued: the call returns {job_id, status: "queued"}, and the result below comes back
under result from get_job_result(job_id) — poll that, never call change_risk again to
wait, which queues a second run. It reads git and the pull requests on GitHub,
the repository's GitHub issues and, when the tenant has the CloudAEye Jira app, the Jira bug
tickets in the window. The only model spend is the slop review of this pull request, and only
when none exists for its current commit — one already run, or a risk card already on the pull
request for that commit, is reused.
Returns:
| Field | Meaning |
|---|---|
card |
The rendered card. Print it verbatim — it leads with the verdict, then what needs attention, then the evidence week by week, and names what it could not measure |
score |
The risk score out of 100, as the card prints it. Not calibrated yet, and the card says so |
band |
low, medium or high; empty when too little was measured to name one — never read that as low |
verdict |
review (human review required) or approve (no extra review needed) |
next_step |
The one action to suggest, chosen by rule from what decided the verdict |
slop |
This pull request's slop score: score, band, and ran — ran when the review was run for this card, reused when an earlier one at this commit was read. {measured: false, reason} when it could not be scored, which rules out approve |
unmeasured |
Each input that could not be measured, as {what, why}. Left out of the score, never to be reported as clean |
reviewers |
Up to two people to ask, as {name, why}. name is a GitHub login only where the card prints it with @ |
census |
What the window held: bugs (bugs, mapped, unmapped — no note yet saying which files the bug was in, so not in the score — and the sources read) and author_prs |
pr_number / pr_url |
The pull request |
posted |
Only with post: true. status is ok when the card is on the pull request; anything else means it is not, and says why |
Every call answers in this shape, whether or not a card is already on the pull request. How the score is built — weights, points per row — is not part of the result or the card.
describe_change¶
A plain-markdown description of the session's diff.
describe_change(session_id: str, intent: str = "", context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session |
intent |
Optional. The user's original task request, verbatim |
context |
Optional extras (pr_title, pr_description) |
Returns: a description field — a # Change Description paragraph plus an ## Important Changes bullet list. Trivial diffs short-circuit to a one-line "Trivial change —" summary. Also returns files_visited, an eval_summary, and a context_refresh summary when the graph refresh was skipped or failed.
ask¶
Answers a free-form question about the session's diff, against the refreshed code-context graph.
ask(session_id: str, question: str, intent: str = "",
context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session |
question |
The user's question, passed verbatim. Do not summarise, expand or rewrite it — the phrasing carries what they want to know, and a rewritten question gets a different answer |
intent |
Optional summary of what the agent just changed. Context for the question, not its subject |
context |
Optional extras (pr_title, pr_description) |
Returns: a plain-markdown answer plus files_visited. This is not a review — no verdict, no findings. For findings use inspect_diff.
check_task¶
Verifies whether the session's diff fulfils a stated task.
check_task(session_id: str, task_description: str,
task_source: str = "user-supplied",
intent: str = "", context: dict | None = None) -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session |
task_description |
The task to verify against — a ticket reference list ([BETA-5225, #42], a bare Jira key, owner/repo#42, or a GitHub issue URL), or freeform text passed verbatim |
task_source |
Provenance label: github-issue, jira, spec, or user-text |
intent |
Optional. A one-line note on this round of edits; on a recheck, which prior gaps were addressed |
context |
Optional extras (pr_title, pr_description) |
Returns: a report field — a # Task Completion Check heading, an overall verdict (DONE / PARTIAL / NOT DONE), a per-requirement checklist, and any gaps. Tickets are re-fetched on every call.
Errors: returns a structured { "error": … } rather than a verdict when task_source=github-issue but the URL is unrecognised, when a ticket cannot be resolved, or when a task made only of unresolvable ticket references has nothing to judge. See Custom Context.
create_issue¶
Files findings from the session's last review as tracking issues.
create_issue(session_id: str, request: str, provider: str = "jira",
assignee: str = "", note: str = "") -> str
| Argument | Description |
|---|---|
session_id |
Id from POST /session. A review must already have run on it; the findings come off its stored report, by the numbers that report printed |
request |
Which findings — "[2]", "[1,3]", "[2-4]" — in implement's grammar, resolved in Python against the stored report. Required, and free text is refused: a wrong number here puts somebody's name on a ticket about the wrong defect. Unlike implement, an empty request is refused rather than read as "all of them" |
provider |
jira (default) or github. Jira goes through the CloudAEye external service and works on a working-tree session as well as a pull-request one; GitHub is the one direct write this server makes to a repository, and files against the session's repository |
assignee |
Optional. A Jira display name or a GitHub username |
note |
What the user wants said — goes above the finding in the description, as the instruction the finding is evidence for |
Returns: three lists, each entry carrying the finding's number and title:
| Field | Meaning |
|---|---|
filed |
Ticket key and link for each finding filed this call. A GitHub entry may carry assignee_dropped — the issue exists, unassigned, because that user cannot be assigned on that repository (HTTP 422; nothing else is retried as an assignee problem) |
already_filed |
Findings that already have a ticket from this session, with its key. Matched by content — path and title — not by number, so a later review renumbering a finding does not file it twice |
failed |
The error as written. no Jira integration is configuration; issues are disabled on this repository is a GitHub setting |
The record lives in the session (filed_issues.json), not in the review report, so it survives a re-review. Filing never closes a finding.
Session-free tools¶
Three tools take no session_id. They name a repository and a pull request, and the hosted CloudAEye service does the rest — fetches the pull request, runs the job, and posts the result on the pull request through the CloudAEye GitHub App. This server holds no repository credential of its own; the App does, and the only thing this server contributes is what to post. The tenant comes from your OAuth token, never from an argument.
All three refuse the same pull requests the review tools refuse: not open, not merging into the integrated branch, from a fork, or over 50 files. Each answers status: "error" with pr_ineligible and a message naming the rule.
All three can outlive one call on a large pull request. They answer status: "pending" with an id — review_id for check_pr, job_id for the generators — and the caller resumes with that id rather than calling again without it: a second review is billed again and posts its own comments; a second generation run posts a second set of suggestions.
check_pr¶
Runs the repository's configured hygiene checklist over an open pull request.
check_pr(repo: str, pr_number: int, review_id: str = "", wait_seconds: int = 0) -> str
| Argument | Description |
|---|---|
repo |
Repository name, as passed to POST /session |
pr_number |
The pull request to check |
review_id |
Resume a review this tool already started — the value from a pending result |
wait_seconds |
How long to wait before returning pending. Defaults to the server's budget |
Which checks run is the repository's configuration in the CloudAEye console. There is no argument to enable one, and none is reachable through the API this calls. With nothing enabled the answer is status: "not_configured", with available_checks, before anything is spent — not an empty checklist that renders like a clean one.
Returns:
| Field | Meaning |
|---|---|
verdict |
approve or request_changes, synthesised here from the check statuses — the service suppresses its own verdict in checklist-only mode. error when the run could not read the pull request's source: report a failed run, print nothing else |
summary |
One line, with the counts and the percentage of any failing coverage check. Never reads clean when degraded is non-empty |
checks |
One row per check: label, status, metric. passed ran and is satisfied; failed ran and found something; skipped is not enabled; error could not run — a coverage gap, not a pass. metric is the check's own number (57.1% (4/7 documented)) or ""; never a figure computed client-side |
counts.findings / stages |
Counts only. The findings' bodies and each check's report are not returned — they are posted on the pull request |
posted |
Whether that posting worked. ok: the detail is on the pull request. partial: some of it. failed: none of it — the table is the whole result. unavailable: the App is not installed on this repository |
pr_url |
Constructed from the repository and number, never read from a payload, so it is present on a resume too |
degraded |
Always carried when non-empty. Checks that could not run |
What crosses the wire is the headline, the table and the link. The full result ran ~4,900 tokens, 77% of it per-check markdown the caller is told never to print; trimmed, ~335. That markdown is what gets assembled into the pull-request comment instead.
add_docs¶
Proposes docstrings for the undocumented code a pull request touches, on the pull request.
add_docs(repo: str, pr_number: int, job_id: str = "", wait_seconds: int = 0) -> str
| Argument | Description |
|---|---|
repo / pr_number |
As for check_pr |
job_id |
Resume a run this tool already started — the value from a pending result |
wait_seconds |
As for check_pr |
Returns:
| Field | Meaning |
|---|---|
summary |
One line with the counts, as the service wrote it |
counts.suggestions |
What the service generated |
files |
Per-file rows: path, suggestions, and new_file for a whole new file |
posted |
What reached the pull request — a different number, and the only one that sends anyone to look. comments is how many landed; fallbacks, within it, how many went up as plain comments because their line is outside the diff (GitHub refuses a suggestion there), so the text is present and the apply button is not. status as for check_pr |
unattached |
Suggestions that came back with no file, when any |
The generated text is never returned. An earlier version of this tool returned it to the coding agent to apply — a docstring written by a model on the server, read by a model on the client, and retyped into an edit, with an unbounded payload. The shape that survives that objection is the service's own: the suggestion is posted where a click applies it, and no diff passes through a context window. A change that starts returning suggestion bodies re-creates the deleted tool.
Zero suggestions is an ordinary answer on a well-documented change, reported as "nothing was added" and never as a pass. empty_result_message is always sent, so a run that generates nothing still leaves a trace on the pull request.
add_tests¶
Proposes unit tests for the changed code nothing covers, on the pull request.
add_tests(repo: str, pr_number: int, job_id: str = "", wait_seconds: int = 0) -> str
Same arguments, same return shape and same rules as add_docs, with two differences:
filesrows markednew_fileare whole new test files. GitHub takes no inline suggestion on a path outside the diff, so those go up as plain comments — the same fallback out-of-diff lines take.- Zero suggestions means nothing in the change was found that a new test would cover. That is not "the code is tested", and the docstring says so in as many words.
The stored result is keyed by (tenant, repository, pull request), not by job — one document, shared by every run of either generator and by @cloudaeye /test on the pull-request timeline. A finished document is accepted only when its job_id matches the one this call's request returned, so a concurrent run finishing while this one polls cannot hand back its counts.
Deriving the diff¶
Always derive the diff from git diff rather than constructing it manually, so the server sees exactly what git sees.
git add --intent-to-add . && git diff > session.diff
git add --intent-to-add is non-destructive — it registers new files in the index without staging their content, so git diff emits them as new file hunks. Without it, completely untracked files are invisible to the diff and to the review. Undo with git reset HEAD <file> if needed.
End-to-end example¶
CE=https://api.cloudaeye.com/mcp
REPO=$(basename -s .git "$(git config --get remote.origin.url)")
BRANCH=$(git rev-parse --abbrev-ref HEAD)
HEAD_SHA=$(git rev-parse HEAD)
# 1. Create or resume the session. The response names the baseline branch.
curl -s -X POST "$CE/session" \
-H "Content-Type: application/json" \
-H "X-Product-API-Key: $CLOUDAEYE_API_KEY" \
-d "{\"repo\":\"$REPO\",\"branch\":\"$BRANCH\",\"head\":\"$HEAD_SHA\",\"tenant_key\":\"$CLOUDAEYE_TENANT_KEY\"}" \
> session.json
SESSION_ID=$(python -c "import json;print(json.load(open('session.json'))['session_id'])")
TARGET=$(python -c "import json;print(json.load(open('session.json')).get('target_branch') or '')")
# 2. Resolve the baseline: the fork point off the integrated branch, not its tip.
# Validate $TARGET first — it comes off the wire, and a leading dash reaching
# git fetch is command execution. No integration, no branch: fall back to HEAD.
BASE=""
case "$TARGET" in -*|*[!A-Za-z0-9._/-]*) TARGET="";; esac
if [ -n "$TARGET" ]; then
git rev-parse --verify -q "origin/$TARGET" >/dev/null || git fetch -q origin "$TARGET"
BASE=$(git merge-base "origin/$TARGET" HEAD)
fi
[ -n "$BASE" ] || BASE=$HEAD_SHA
# 3. Capture and upload the diff, with the commit it was taken against.
git add --intent-to-add . && git diff "$BASE" > session.diff
curl -s -F "file=@session.diff" -F "base_sha=$BASE" \
-H "X-Product-API-Key: $CLOUDAEYE_API_KEY" \
"$CE/upload/$SESSION_ID"
# 4. Call inspect_diff via your MCP client, passing session_id + intent + profile
Steps 1–3 are plain HTTP; step 4 is an MCP tool call your client makes. The Claude Code plugin bundles all four into one slash command; a client without that mechanism runs the first three itself.