Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(body): accept --body @file and validate JSON client-side by dspangen · Pull Request #75 · exploreomni/cli · GitHub
Skip to content

feat(body): accept --body @file and validate JSON client-side - #75

Merged
dspangen merged 7 commits into
mainfrom
feat/body-file-input
Aug 28, 2026
Merged

feat(body): accept --body @file and validate JSON client-side#75
dspangen merged 7 commits into
mainfrom
feat/body-file-input

Conversation

@dspangen

@dspangendspangen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

--body @file.json (curl-style) now works, and bodies are validated as JSON client-side before any network call. Previously a file path passed to --body was POSTed literally and the server's Invalid JSON 400 read as a body-shape problem, sending callers back to --schema (6 audited incidents).

  • --body @path reads the file (10 MB cap, ~/ expansion, @- = stdin); missing file is a clear local error.
  • Invalid JSON fails locally; a path-shaped value gets a transport hint: read the file instead: --body @/tmp/body.json.
  • --body '' is now an error (almost always an unexpanded "$BODY"); omitting the flag still sends no body.
  • Bytes are never re-serialized — what you pass is what gets sent. Multipart endpoints skip JSON validation but keep the path hint.
Details: behavior, edge cases, and verification

Problem

omni ai generate-query --body @/tmp/body.json # curl syntax
omni ai generate-query --body /tmp/body.json # bare path

Both were sent to the API verbatim as the body. The server replied {"detail": "Bad Request: Invalid JSON", "status": 400} — which reads as a body-shape problem, so the agent went back to --schema, rebuilt the JSON, and hit the same wall. The mistake was never about shape; it was about transport.

Changes

  • --body @path/to/file.json reads the file. Same 10 MB cap as stdin, ~/ expanded (shells don't expand it after @), and @- accepted as curl's spelling of stdin. A missing or unreadable file is a client-side error naming the path.

  • Bodies are checked with json.Valid before the request is made. No network call happens on a bad body. Bytes are never re-serialized — what you pass is what gets sent, field order and all.

  • A path-shaped --body gets a transport hint, not a parse error:

    Error: --body looks like a file path, not a request body: /tmp/body.json
    read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
    

    Triggered by a /, ./, ../, ~/ prefix or an existing file — including paths with spaces, which arrive unquoted from the shell and get re-quoted in the suggestion. Anything else malformed gets the parse error plus byte offset and surrounding snippet.

  • --body '' is an error, not a bodiless request. Omitting the flag still sends no body.

  • Applies identically to the hidden --json-body alias; errors name whichever flag was typed.

  • The multipart/form-data upload operations skip JSON validation only (operationInfo.BodyNonJSON) — the transport checks still apply there, since --body @file is exactly what those endpoints need.

  • Help text updated in generate.go, omni agent-help, and the README.

Body shorthand (omni ai search-omni-docs "question") is unaffected: it assembles its body with json.Marshal and sets --body internally, so it passes validation untouched. Shorthand commands also accept --body @file.

Verification

make build && make test — all packages pass, including the existing spec-coverage test over every operation in api/openapi.json.

25 tests in internal/openapi/body_input_test.go cover: @file expansion, missing / oversized / non-JSON @file, bare @, the file-path hint for all four path shapes plus a space-containing existing path, the parse diagnostic, flag naming, byte-identical passthrough of valid JSON (including whitespace and non-object bodies), the non-JSON media-type carve-out and its retained path hint, stdin via - and @-, empty sources, --body '' on both plain and shorthand commands, @file end-to-end through a generated cobra command, the executor not running on an invalid body, and both shorthand paths.

Manual check against a real binary:

$ omni ai search-omni-docs --body /tmp/body.json
Error: --body looks like a file path, not a request body: /tmp/body.json
read the file instead: --body @/tmp/body.json (or --body - < /tmp/body.json)
$ omni ai search-omni-docs --body '{"question":"x",}'
Error: request body from --body is not valid JSON: invalid character '}' looking
for beginning of object key string (at byte 17, near "{\"question\":\"x\",}")
$ omni query run --body ''
Error: --body is empty; omit the flag to send no body
$ omni uploads create --body /tmp/upload.csv
Error: --body looks like a file path, not a request body: /tmp/upload.csv
read the file instead: --body @/tmp/upload.csv (or --body - < /tmp/upload.csv)
$ omni ai search-omni-docs --body @/tmp/body.json # reaches the API

🤖 Generated with Claude Code

https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

dspangen added a commit that referenced this pull request Aug 24, 2026
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
dspangenand others added 2 commits August 24, 2026 12:23
Agents reach for curl syntax (--body @/tmp/body.json) or pass a bare file
path. Both were sent verbatim as the request body, and the API answered
{"detail": "Bad Request: Invalid JSON"} — a message that reads like a
body-SHAPE problem and sends the caller back to re-read the schema for a
mistake that was purely about transport.
--body/--json-body now resolve "@path" (and curl's "@-") to file contents
under the same 10 MB cap as stdin, and every JSON-media-type body is run
through json.Valid before any network call. A value that looks like a path
(/, ./, ../, ~/ prefix, or an existing file) gets an error naming both
working forms instead of a parse error.
Constraint: bytes must reach the server unchanged — validation uses json.Valid and never re-serializes, so field order and formatting survive
Constraint: body shorthand sets --body internally to marshaled JSON; that path stays valid and untouched
Rejected: schema-aware validation of the body | needs the full JSON Schema evaluator and would reject bodies the API actually accepts
Rejected: silently treating a bare existing path as a file | hides the typo class this is meant to surface, and changes what an existing script sends
Confidence: high
Scope-risk: narrow
Directive: multipart/form-data operations (uploads) skip validation via operationInfo.BodyNonJSON — keep that carve-out if more media types appear
Not-tested: reading from a FIFO or /dev/stdin via @path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Review of #75 found three ways the new diagnostics went unreached:
1. `--body ''` never got validated — the resolve step keyed off a non-empty
string, so the explicit-empty error was dead code. It now keys off
Flag.Changed, which distinguishes "flag omitted" (send no body, unchanged)
from "flag typed as empty" (usually an unexpanded shell variable). The
shorthand wrapper defers to the same path instead of quietly assembling a
body from positional args.
2. A quoted path with spaces got a JSON parse error instead of the hint —
looksLikePath bailed on whitespace before it checked prefixes or the
filesystem, but the shell strips the quotes, so those values arrive looking
ordinary. The whitespace guard now only narrows the stat() branch, to
newlines, and the suggested commands are re-quoted so they paste back.
3. Multipart operations skipped path detection along with JSON validation, so
the endpoints where "--body @file" is the whole point sent the path
literally. Transport checks (empty input, path-shaped value) now run for
every media type; only json.Valid stays conditional.
Constraint: valid JSON is tested before path-shape, so a file that happens to
be named "{}" can't hijack a legitimate body
Rejected: treating `--body ''` as "send no body" | it is indistinguishable
from an unexpanded "$BODY" and silently posts nothing
Confidence: high
Scope-risk: narrow
Not-tested: a shorthand command given `--body ''` and zero positional args —
the arg validator rejects it first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds file-backed body input and client-side JSON validation to generated API commands.

Changes:

  • Supports --body @file, @-, home expansion, and size limits.
  • Validates JSON locally with clearer diagnostics.
  • Adds comprehensive tests and updates help documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
README.mdDocuments body input options.
internal/openapi/generate.goIntegrates body resolution and media-type detection.
internal/openapi/body_shorthand.goHandles explicit body flags with shorthand commands.
internal/openapi/body_input.goImplements file reading, validation, and diagnostics.
internal/openapi/body_input_test.goTests body-input behavior and integration.
cmd/omni/agent_help.goUpdates agent-facing body guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadinternal/openapi/generate.go Outdated
if op.RequestBody != nil {
info.HasBody = true
info.BodySchema = requestBodySchema(op.RequestBody)
info.BodyNonJSON = !requestBodyIsJSON(op.RequestBody)
Comment threadinternal/openapi/generate.go Outdated
Comment on lines +231 to +232
if jsonBodyFlag != "" || (cmd.Flags().Changed("json-body") && !cmd.Flags().Changed("body")) {
effectiveBody, flagName = jsonBodyFlag, "json-body"
Comment threadinternal/openapi/body_shorthand.go Outdated
Comment on lines +329 to +333
// An explicitly empty --body/--json-body is a typo, not an invitation
// to assemble one from shorthand input. Hand it back to the generated
// RunE so it reports the same error every other command does.
if cmd.Flags().Changed("body") || cmd.Flags().Changed("json-body") {
return originalRunE(cmd, args[:numPathParams])
@n8agrin

Copy link
Copy Markdown
Contributor

Code review found one bug.

internal/openapi/body_shorthand.go:363--body '' on shorthand commands doesn't reach the new "empty body" error.

flexibleArgs decides whether --body was passed by checking bodyFlag != "". An explicitly empty string looks the same as unset, so it falls through to requiring the shorthand positional arg count instead of just the path params. Reproduced on a build of this branch:

$ omni ai search-omni-docs --body ''
Error: accepts 1 arg(s), received 0

That's exactly the typo case (unexpanded --body "$VAR") this PR is meant to catch — on shorthand commands it never reaches the "--body is empty" message added at body_shorthand.go:332.

@n8agrinn8agrin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one finding from the bots

dspangenand others added 2 commits August 24, 2026 21:45
Body-mode selection compared flag values, so `--body ''` looked unset:
shorthand commands demanded their positional args and never reached the
"--body is empty" diagnostic, and `--body '' --json-body '{}'` slipped past
the mutual-exclusion check. Both now key off Changed.
Multipart uploads were advertised as callable with `--body @file`, but
auth.Do labels every request application/json and the CLI builds no part
framing, so those bytes could never satisfy the API. Those operations now
fail client-side naming the media type and printing the equivalent curl
command, and the help text, agent-help and README say the same.
Constraint: auth.Do carries no media type — a body is always sent as application/json
Rejected: implement real multipart transport | needs media type plumbing through APIRequest plus part construction; out of scope for a bug-fix pass
Rejected: send raw bytes for multipart ops as before | mislabeled and unframed, guaranteed API rejection dressed up as CLI support
Confidence: high
Scope-risk: narrow
Directive: if multipart is ever implemented, drop unsupportedBodyError together with the help/README caveats — they must not outlive it
Not-tested: no live API call against the uploads endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TwwKSAsAGPBToNb4iUe5s
Constraint: 3d9b0e1 refused multipart client-side; its directive says drop unsupportedBodyError when multipart lands
Rejected: keep the refusal for uploads not yet verified | end-to-end round trip against a local server confirms framing and Content-Type
Confidence: high
Scope-risk: moderate
Directive: --body on a multipart op is a JSON map of field values, not raw file bytes — bodyFlagIsJSON() must keep returning true for multipart or #75's @file reading and validation stop applying there
Not-tested: no live call against the real Omni uploads API
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dspangen

Copy link
Copy Markdown
ContributorAuthor

@ernestoongaro#72 will land with this one.

Main landed flag normalization (#77) and --schema everywhere (#79), both of
which overlap this branch's generate.go work.
Resolutions:
- Query/path flag naming: main's canonicalName + SetNormalizeFunc +
resolveQueryFlags replaces this branch's cliFlagName/queryFlagValue and the
hidden deprecated legacy aliases. Main's approach covers the same spellings
(--modelid, --modelId, --model_id) via pflag normalization, so the alias
flags and their conflict check are no longer reachable behavior.
- operationInfo: kept both sides' fields (BodyMediaType/BodyFields from here,
Response from main).
- requestBodySchema: dropped in favor of this branch's requestBodyMediaType,
which also returns the MediaType so multipart encodings stay reachable.
- agent-help / README: kept both sides' text (--body @file and multipart from
here, flag-spelling and --schema-everywhere notes from main).
- Removed TestBuildCommand_CamelCaseQueryFlags: it asserts the legacy-alias
behavior main replaced. Main's TestBuildCommand_AlternateFlagSpellings and
TestBuildCommand_CamelCaseQueryParamBecomesKebabFlag cover the successor.
Rejected: keeping legacy alias flags alongside main's normalization | duplicate
mechanisms for one behavior, and the aliases would shadow the normalize func
Confidence: high
Scope-risk: moderate
Directive: multipart FlagName now uses canonicalName; keep it in step with
resolveQueryFlags so upload flags and query flags stay spelled alike
Not-tested: multipart upload against a live API after the flag-name change
@ernestoongaro

Copy link
Copy Markdown
Collaborator

Opened #82 into feat/body-file-input — the multipart review findings from #72 that 3d9b0e1 and 0827738 didn't already cover.

Two are real bugs: ~ was never expanded for binary field paths, so --file ~/people.csv failed with "no such file or directory" while --body @~/x.json worked; and --body null decoded into a nil map, panicking with assignment to entry in nil map on the first generated flag merged into it. Three more are latent on the current spec — flag-name collisions could register the same pflag twice and panic at startup, array/object flag values decoded into interface{} so an object passed where the schema says array and trailing data was dropped, and requestBodyMediaType could prefer a schema-less application/json entry over a real multipart definition.

Regression test per fix; go test ./... green on top of 5eea40d. Stacked as a PR rather than pushed onto your branch, matching #74/#77/#79.

Closing #72 as superseded — its multipart commit already landed here as ffebcee.

One thing I left alone: operationInfo.bodyFlagIsJSON (generate.go:448) is dead now that 3d9b0e1 dropped resolveBody's validateJSON parameter, and its comment still describes the old behavior. Yours to cut or keep.

…82)
Follow-up to the multipart work now carried on this branch (ffebcee).
`3d9b0e1` and `0827738` already covered the body-flag Changed state and the
file-path hint; these are the findings from #72's review that no commit here
has picked up yet.
- Binary multipart field paths never expanded `~`, unlike `--body @path`, so
`--file ~/people.csv` failed with "no such file or directory".
- `--body null` decoded into a nil map and panicked ("assignment to entry in
nil map") as soon as any generated flag was merged into it.
- Array and object flag values decoded into interface{}, so an object was
accepted where the schema says array, and anything after the first JSON
value was silently dropped: `--labels '["a"] oops'` sent `["a"]`. The
declared type is pinned now and the input must end there.
- registerMultipartFlags checked its "form-" replacement against nothing, so
two fields colliding on one flag name would register the same pflag twice
and panic at startup, taking down every command, not just the upload.
- requestBodyMediaType could prefer a schema-less application/json entry over
a real multipart definition.
Each fix has a regression test; without the source changes the tilde and
nil-map tests fail, the latter by panicking.
Not addressed: Copilot's note that the upload file is buffered in memory
before the request is sent. Streaming means threading an io.Reader through
APIRequest and internal/auth, which is wider than a review-fix pass.
Claude-Session: https://claude.ai/code/session_01DYuiGGkmQifkCbF2qL8Lt6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/openapi/generate_test.go
@dspangen
dspangen merged commit 8500aac into mainAug 28, 2026
2 checks passed
@dspangen
dspangen deleted the feat/body-file-input branch August 28, 2026 12:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dspangen@n8agrin@ernestoongaro