Skip to content

feat(cli): make onyx-cli agent-friendly#9874

Open
rohoswagger wants to merge 17 commits intomainfrom
cli-agent-friendly
Open

feat(cli): make onyx-cli agent-friendly#9874
rohoswagger wants to merge 17 commits intomainfrom
cli-agent-friendly

Conversation

@rohoswagger
Copy link
Copy Markdown
Contributor

@rohoswagger rohoswagger commented Apr 2, 2026

Description

Makes the onyx-cli significantly more useful when invoked by AI agents and scripts, based on patterns from CLI-for-agents design articles.

Stdin support for ask:

  • Pipe content via stdin as context: cat error.log | onyx-cli ask --prompt "find the root cause"
  • Stdin-only mode: echo "what is onyx?" | onyx-cli ask
  • Positional arg + stdin combined: cat log | onyx-cli ask "summarize this"
  • 10MB stdin cap to prevent OOM

Auto-truncate for non-TTY callers:

  • When stdout is not a TTY (agent/script calling), output auto-truncates at 4KB
  • Full response saved to temp file with exploration hints (grep, tail)
  • --max-output N to override threshold, --max-output 0 to disable
  • Human terminal use is unchanged

Semantic exit codes:

  • 0 success, 2 not configured, 3 auth failure, 4 unreachable, 5 bad request
  • New exitcodes package with ExitError type, extracted in main.go

Non-interactive configure:

  • onyx-cli configure --server-url X --api-key Y saves config without prompts
  • --dry-run tests connection without saving

Other improvements:

  • --quiet/-q flag on ask to buffer output and print once (no streaming)
  • Actionable error messages with remediation hints on all commands
  • Help examples added to every subcommand
  • Consistent error message formatting

How Has This Been Tested?

  • 12 new unit tests: resolveQuestion (3 cases), overflowWriter (5 cases including quiet mode), exitcodes (4 cases)
  • All existing tests pass (go test ./...)
  • go vet and golangci-lint clean
  • Manual verification of --help output for all commands

Additional Options

  • [Optional] Please cherry-pick this PR to the latest release version.
  • [Optional] Override Linear Check

Summary by cubic

Make the CLI easier for agents and scripts with stdin-aware ask, disk-backed auto-truncate, non-interactive configure (implicit or explicit stdin via --api-key-stdin, --dry-run), quiet mode, semantic exit codes, and TTY-only streaming status lines (suppressed in --quiet).

  • New Features

    • ask: accept piped stdin (works with arg or --prompt); --quiet for non-streaming; auto-truncate non-TTY output to 4KB by default (--max-output, 0 disables); full response saved to a temp file.
    • ask: show dim search/reasoning/tool status on stderr while streaming, only when stdout is a TTY; suppressed with --quiet.
    • configure: non-interactive via --server-url and --api-key, or read the API key from stdin when omitted; --api-key-stdin for explicit piping; validates connection before saving; --dry-run to test without saving.
    • Semantic exit codes via exitcodes (2 bad request, 3 not configured, 4 auth failure, 5 unreachable) with clearer help and remediation hints.
  • Bug Fixes

    • ask: cap stdin at 10MB; stream overflow directly to disk; reject --json + --quiet; reject conflicting arg + --prompt; avoid trailing newline in JSON output; fall back to stdout if temp file write fails.
    • configure/validate-config: distinguish auth failures from unreachable via *api.AuthError and return codes 4 vs 5.
    • configure: don’t persist env var overrides when saving.

Written for commit 2c28b57. Summary will update on new commits.

@rohoswagger rohoswagger requested a review from a team as a code owner April 2, 2026 21:20
@rohoswagger rohoswagger changed the title fix(cli): cap stdin read at 10MB and remove duplicate serve examples feat(cli): make onyx-cli agent-friendly Apr 2, 2026
rohoswagger and others added 4 commits April 2, 2026 14:21
…d non-interactive configure

Phase 1 of agent-friendly CLI improvements:

- ask: support piped stdin input with --prompt flag for separating question
  from context (arg+stdin, prompt+stdin, stdin-only all work)
- ask: auto-truncate output at 4KB when stdout is not a TTY (agent calling),
  save full response to temp file with exploration hints. --max-output to
  override threshold, --max-output 0 to disable
- configure: accept --server-url and --api-key flags for non-interactive
  setup (tests connection before saving)
- All commands: actionable error messages with remediation hints
- All commands: help examples added to every subcommand
- New unit tests for resolveQuestion and overflowWriter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 of agent-friendly CLI improvements:

- New exitcodes package with typed ExitError: agents can distinguish
  not-configured (2), auth failure (3), unreachable (4), bad request (5)
- main.go extracts exit codes via errors.As for proper os.Exit
- All commands updated to return typed ExitError where appropriate
- ask: add --quiet/-q flag to buffer output and print once at end
  (no streaming chunks, useful for scripting)
- Unit tests for exitcodes package and quiet mode overflow writer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allows testing a server URL and API key combination without saving the
config. Useful for agents and scripts that want to validate credentials
before committing to a configuration change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review fixes:
- ask: limit stdin reads to 10MB to prevent OOM on large pipes
- serve: remove duplicate examples from Long description (Cobra
  renders the Example field separately)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 8 files

Confidence score: 3/5

  • There is a concrete user-facing regression risk in cli/cmd/ask.go: combining --json with --quiet can return success with empty stdout, which can break automation expecting JSON events.
  • cli/cmd/ask.go also still buffers full responses before truncation, so long outputs may cause significant memory growth under real usage.
  • cli/cmd/configure.go may accidentally persist ONYX_PERSONA_ID via config.Load(), creating unexpected config state by saving env-derived values as defaults.
  • Pay close attention to cli/cmd/ask.go and cli/cmd/configure.go - output semantics and config persistence behavior need validation before merge.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/cmd/configure.go">

<violation number="1" location="cli/cmd/configure.go:70">
P2: Using `config.Load()` here can persist `ONYX_PERSONA_ID` into the saved config. Read the existing value from disk-only config (without env overrides) before preserving `DefaultAgentID`.</violation>
</file>

<file name="cli/cmd/ask.go">

<violation number="1" location="cli/cmd/ask.go:113">
P1: `--json` + `--quiet` currently drops all output. JSON mode should still emit events (or reject the flag combination) instead of returning success with empty stdout.</violation>

<violation number="2" location="cli/cmd/ask.go:216">
P2: Truncation still buffers full responses in memory, which can cause large memory growth on long outputs.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Preview Deployment

Status Preview Commit Updated
https://onyx-preview-arvkzdtxx-danswer.vercel.app 718227a 2026-04-02 21:25:36 UTC

rohoswagger and others added 3 commits April 2, 2026 14:26
Every command now has a Long description for --help consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Using both flags silently dropped all JSON events, returning exit 0 with
empty stdout. Now rejects the combination upfront with a clear error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
overflowWriter now opens a temp file eagerly when limit > 0 and streams
chunks directly to disk. Previously it accumulated the full response in
a strings.Builder, which could cause large memory growth on long outputs.
The in-memory buf is now only used for quiet mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 2, 2026

Greptile Summary

This PR makes the onyx-cli significantly more useful for AI agents and scripts by adding stdin piping for ask, disk-backed auto-truncation for non-TTY callers, non-interactive configure, a --quiet mode, semantic exit codes via a new exitcodes package, and consistent actionable error messages across all subcommands.

Key changes:

  • cli/internal/exitcodes/codes.go — new package with typed ExitError and named exit code constants, correctly unwrapped in main.go to call os.Exit with the right code
  • cli/internal/overflow/writer.go — new disk-backed streaming writer that caps stdout at --max-output bytes and saves the full response to a temp file; truncation metadata is written to stdout rather than stderr (flagged below)
  • cli/cmd/ask.goresolveQuestion cleanly handles all combinations of positional arg, --prompt, and piped stdin; status events (search, reasoning, tools) correctly go to stderr; JSON mode is properly gated before the overflow writer
  • cli/cmd/configure.go — non-interactive path correctly detects *api.AuthError to emit distinct exit codes for auth failure vs unreachable; implicit stdin reading for the API key can consume stdin before the interactive wizard runs (flagged below)
  • cli/internal/api/client.go and errors.goTestConnection now returns typed *AuthError for 401/403 responses, enabling callers to distinguish auth failures from unreachable servers
  • cli/internal/config/config.go — correctly extracts LoadFromDisk() to preserve the on-disk agent ID without being polluted by environment variable overrides during a save

Issues found:

  • The truncation footer in overflow/writer.go is printed to stdout via fmt.Printf, mixing metadata into the agent-readable response stream; all status lines elsewhere go to os.Stderr and this footer should too.
  • In configure.go, when stdin is piped but --server-url is not provided, io.ReadAll(os.Stdin) consumes stdin before falling through to onboarding.Run, which then receives EOF and breaks the interactive wizard.
  • Minor: configure.go has no size limit on the stdin read (unlike the cap in ask.go), and TestWriter_UnderLimit leaks a temp file because Finish() is never called.

Confidence Score: 4/5

Safe to merge after addressing the two P1 issues: truncation metadata going to stdout and stdin consumed before interactive wizard.

Two P1 findings remain: (1) the truncation notice goes to stdout instead of stderr, directly undermining the agent-friendly goal of this PR by mixing metadata into the response stream, and (2) piping empty stdin to configure without flags consumes stdin before onboarding.Run is called, breaking the interactive wizard. Both are correctness issues on changed code paths. All prior review thread concerns (missing exitcodes package, prompt conflict, AuthFailure unreachable) are addressed in this revision.

cli/internal/overflow/writer.go (truncation notice to stdout) and cli/cmd/configure.go (stdin consumed before interactive fallback)

Important Files Changed

Filename Overview
cli/cmd/ask.go Major refactor adding stdin piping, quiet mode, TTY-based auto-truncation, and semantic exit codes; logic for resolveQuestion and JSON/non-JSON flow is sound; overflow writer integration is correct except the truncation notice goes to stdout instead of stderr.
cli/cmd/configure.go Adds non-interactive configure with AuthError detection; stdin-consumed-before-interactive-fallback bug breaks the wizard when empty stdin is piped without flags; also missing size cap on stdin read.
cli/internal/overflow/writer.go New streaming writer with disk-backed truncation; core logic is correct, but the truncation metadata is printed to stdout instead of stderr, which contaminates agent-readable output contrary to the PR's stated goal.
cli/internal/overflow/writer_test.go Good coverage for truncation modes; TestWriter_UnderLimit leaks a temp file because Finish() is never called after writes with Limit > 0.
cli/internal/exitcodes/codes.go New package defining semantic exit codes and ExitError; implementation is clean and correctly wired into main.go.
cli/internal/api/client.go TestConnection now returns typed *AuthError for 401/403 responses, enabling callers to distinguish auth failures from unreachable servers.

Sequence Diagram

sequenceDiagram
    participant Caller as Agent / Script
    participant main as main.go
    participant ask as cmd/ask.go
    participant ow as overflow.Writer
    participant api as api.Client
    participant tmp as Temp File

    Caller->>main: onyx-cli ask [question]
    main->>ask: RunE()
    ask->>ask: resolveQuestion(args, --prompt, stdin)
    ask->>api: SendMessageStream(ctx, question)
    api-->>ask: event channel

    loop stream events
        ask->>ow: Write(content)
        ow->>tmp: WriteString(full content)
        alt written < Limit
            ow-->>Caller: fmt.Print(content) to stdout
        else over Limit
            ow->>ow: truncated = true
        end
    end

    ask->>ow: Finish()
    alt not truncated
        ow->>tmp: Remove (cleanup)
        ow-->>Caller: fmt.Println() to stdout
    else truncated
        ow-->>Caller: response truncated notice to stdout WRONG
        ow-->>Caller: Full response path to stdout WRONG
        Note over ow,Caller: Metadata should go to stderr
    end

    alt ExitError returned
        main->>main: errors.As exitErr.Code
        main->>Caller: os.Exit(code)
    else generic error
        main->>Caller: os.Exit(1)
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: cli/internal/overflow/writer.go
Line: 100-104

Comment:
**Truncation metadata written to stdout contaminates agent output**

The truncation notice and exploration hints are printed with `fmt.Printf` to stdout. For the stated agent-friendly goal, this is self-defeating: an AI agent reading stdout captures both the response content and the metadata as a single stream, making it impossible to cleanly separate the actual answer from the "file was truncated" advisory.

All progress/status messages in the stream handler (`SearchStartEvent`, `SearchQueriesEvent`, etc.) already correctly use `fmt.Fprintf(os.Stderr, ...)`. The truncation footer should follow the same pattern:

```go
fmt.Fprintf(os.Stderr, "\n\n--- response truncated (%d bytes total) ---\n", w.totalBytes)
fmt.Fprintf(os.Stderr, "Full response: %s\n", tmpPath)
fmt.Fprintf(os.Stderr, "Explore:\n")
fmt.Fprintf(os.Stderr, "  cat %s | grep \"<pattern>\"\n", tmpPath)
fmt.Fprintf(os.Stderr, "  cat %s | tail -50\n", tmpPath)
```

With this change, `stdout` stays clean machine-readable text and `stderr` carries the advisory, which is consistent with standard Unix tool conventions and the rest of the PR.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: cli/cmd/configure.go
Line: 52-74

Comment:
**Stdin consumed before interactive fallback makes onboarding wizard hang on EOF**

When stdin is piped but neither `--server-url` nor `--api-key` is provided (e.g. `echo "" | onyx-cli configure` or `cat /dev/null | onyx-cli configure`), the logic at line 52 detects a non-TTY stdin and calls `io.ReadAll(os.Stdin)`, consuming the entire stream. After trim, `apiKey` is `""`, and since both `serverURL` and `apiKey` are empty, none of the early-return guards fire and execution falls through to `onboarding.Run(&cfg)`.

`onboarding.Run` drives an interactive prompt wizard that reads from stdin — but stdin is now at EOF. Depending on the prompt library, this either panics, loops infinitely reading empty input, or silently produces a corrupt config.

The simplest fix is to guard stdin reading behind the condition that `serverURL` is already provided (i.e., we're actually attempting a non-interactive flow):

```go
// Only read API key from stdin when --server-url was also given or --api-key-stdin was explicit.
// Unconditionally consuming stdin breaks the interactive wizard if the user runs
// `echo "" | onyx-cli configure` without flags.
if apiKeyStdin || (apiKey == "" && serverURL != "" && !term.IsTerminal(int(os.Stdin.Fd()))) {
    data, err := io.ReadAll(os.Stdin)
    ...
}
```

This ensures the interactive path always has a live stdin.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: cli/cmd/configure.go
Line: 53

Comment:
**No stdin size cap when reading the API key**

`io.ReadAll(os.Stdin)` has no upper bound. `ask.go` guards the equivalent stdin read with a 10 MB `io.LimitReader`. An API key is always tiny, so in practice this is harmless, but for consistency and defensive coding the same pattern should be applied here — even a small limit like 64 KB would prevent an accidental `cat large_file | onyx-cli configure` from allocating unbounded memory:

```suggestion
			data, err := io.ReadAll(io.LimitReader(os.Stdin, 64*1024))
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: cli/internal/overflow/writer_test.go
Line: 19-29

Comment:
**Temp file leaked by `TestWriter_UnderLimit`**

When `Limit > 0`, the very first `Write` call creates a temp file (`onyx-ask-*.txt` under the system temp directory). `TestWriter_UnderLimit` writes 11 bytes with `Limit: 100`, so a temp file is created — but the test never calls `Finish()` or manually cleans it up. Every run of this test leaves an orphaned file in `/tmp`.

The other tests that exercise truncation (`TestWriter_OverLimit`, `TestWriter_MultipleChunks`) both close and remove the temp file explicitly. Apply the same treatment here:

```go
func TestWriter_UnderLimit(t *testing.T) {
    w := &Writer{Limit: 100}
    w.Write("hello")
    w.Write(" world")
    // Clean up the temp file created by the first Write
    if w.tmpFile != nil {
        name := w.tmpFile.Name()
        _ = w.tmpFile.Close()
        _ = os.Remove(name)
    }
    if w.truncated {
        t.Fatal("should not be truncated when under limit")
    }
    if w.written != 11 {
        t.Fatalf("expected 11 written bytes, got %d", w.written)
    }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (6): Last reviewed commit: "fix(cli): suppress status lines in quiet..." | Re-trigger Greptile

cli/cmd/ask.go Outdated
Comment on lines +214 to +218
// Quiet mode: buffer everything, print nothing
if w.quiet {
w.buf.WriteString(s)
return
}
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.

P1 Quiet mode bypasses non-TTY truncation limit

When --quiet is active, Write() accumulates all content in w.buf without any size check, and Finish() prints w.buf.String() in full — completely bypassing the limit field and any truncateAt threshold. This means that when an agent/script runs onyx-cli ask --quiet ..., the auto-enabled 4 KB truncation safety guard is silently ignored, and the full (potentially multi-MB) response is dumped to stdout in one shot.

This is the opposite of the agent-friendly goal described in the PR. The quiet mode accumulation path should still respect the limit:

func (w *overflowWriter) Finish() {
    if w.quiet {
        out := w.buf.String()
        if w.limit > 0 && len(out) > w.limit {
            // truncate and write to temp file, same as non-quiet path
            // ...
        } else {
            fmt.Print(out)
        }
        return
    }
    // ...
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: cli/cmd/ask.go
Line: 214-218

Comment:
**Quiet mode bypasses non-TTY truncation limit**

When `--quiet` is active, `Write()` accumulates all content in `w.buf` without any size check, and `Finish()` prints `w.buf.String()` in full — completely bypassing the `limit` field and any `truncateAt` threshold. This means that when an agent/script runs `onyx-cli ask --quiet ...`, the auto-enabled 4 KB truncation safety guard is silently ignored, and the full (potentially multi-MB) response is dumped to stdout in one shot.

This is the opposite of the agent-friendly goal described in the PR. The quiet mode accumulation path should still respect the limit:

```go
func (w *overflowWriter) Finish() {
    if w.quiet {
        out := w.buf.String()
        if w.limit > 0 && len(out) > w.limit {
            // truncate and write to temp file, same as non-quiet path
            // ...
        } else {
            fmt.Print(out)
        }
        return
    }
    // ...
}
```

How can I resolve this? If you propose a fix, please make it concise.

rohoswagger and others added 2 commits April 2, 2026 14:31
configureNonInteractive used config.Load() which applies ONYX_PERSONA_ID
env overrides, causing the env value to leak into the saved config file.
Now uses LoadFromDisk() to read only persisted values. Also extracts
LoadFromDisk as a reusable function from Load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The exitcodes package was created on disk but never staged/committed
because ez commit -am only picks up tracked files. Adding the new
untracked files that all commands depend on.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/cmd/ask.go">

<violation number="1" location="cli/cmd/ask.go:241">
P2: Handle temp-file write failures instead of silently discarding them; otherwise truncation mode can claim a full response file that was not actually written.

(Based on your team's feedback about warning instead of silently swallowing runtime behavior changes.) [FEEDBACK_USED]</violation>
</file>

<file name="cli/internal/exitcodes/codes.go">

<violation number="1" location="cli/internal/exitcodes/codes.go:36">
P2: `Wrap` allows `nil` errors, which can panic when `Error()` is called on the resulting `ExitError`. Guard `nil` in `Wrap` so `ExitError.Err` is always non-nil.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Passing both a positional argument and --prompt silently dropped the
--prompt value. Now fails with a clear error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

🖼️ Visual Regression Report

Project Changed Added Removed Unchanged Report
admin 0 0 0 165 ✅ No changes
exclusive 0 0 0 8 ✅ No changes

…Wrap

- overflowWriter: check WriteString error and fall back to streaming
  directly to stdout instead of silently producing a truncated temp file
- exitcodes.Wrap: guard nil error to prevent panic in ExitError.Error()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/cmd/ask.go">

<violation number="1" location="cli/cmd/ask.go:252">
P2: Clearing `w.truncated` on temp-file write failure can silently hide already-truncated output and produce partial/misaligned stdout without a truncation notice.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const (
Success = 0
General = 1
NotConfigured = 2
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.

I would make BadRequest two. Convention is typically to use 2 for invalid args/command line errors

return e.Err.Error()
}

func (e *ExitError) Unwrap() error {
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.

Wrap and Unwrap are only used in tests. Maybe we can remove?

return &cobra.Command{
var (
serverURL string
apiKey string
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.

Does this support reading from stdin? It's fairly common to use files/env to avoid leaking api keys in shell history (I would recommend this/put it in the docs as it is more secure). For example, docker has both a --password and --password-stdin, https://docs.docker.com/reference/cli/docker/login/#options

Also, this should probably respect ONYX_API_KEY if it doesn't already

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep added support for pipe, ONYX_API_KEY, and --api-key-stdin

cli/cmd/ask.go Outdated
// When limit > 0, it streams to a temp file on disk (not memory) and stops
// writing to stdout after limit bytes. When limit == 0, it writes directly
// to stdout. In quiet mode, it buffers in memory and prints once at the end.
type overflowWriter struct {
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.

nit: could maybe be moved to a dedicated package. Could be use for future commands and I find it better to refactor before LLMs duplicate it everywhere because they don't know to look in here -- they're much more willing to look at the file tree and especially utils than they are to read whole files.

cli/cmd/ask.go Outdated

// Close the temp file so it's readable
tmpPath := w.tmpFile.Name()
_ = w.tmpFile.Close()
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.

It's probably not a big deal, but we probably should be 1. defering closing files (so it closes even if this function is exited prematurely) and 2. at least logging when files fail to close. I don't think it's likely, but in an agentic world, this may manifest as too many files open errors eventually.

rohoswagger and others added 2 commits April 2, 2026 17:26
- exitcodes: swap BadRequest to code 2 (convention for invalid args),
  shift NotConfigured/AuthFailure/Unreachable to 3/4/5
- exitcodes: remove Wrap/Unwrap (only used in tests, per reviewer)
- configure: add --api-key-stdin to read key from pipe instead of flag,
  avoiding shell history leaks (like docker login --password-stdin)
- overflow: extract overflowWriter to internal/overflow package for
  reuse by future commands and better discoverability
- overflow: use defer-based file cleanup with logged close errors
- ask: guard ow.Finish() with !askJSON to fix spurious blank line
  appended to JSON event stream (Greptile P1)
- ask: add arg+prompt conflict test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop the --api-key-stdin flag. Instead, if --api-key is omitted and stdin
has piped data, read the API key from stdin automatically. Simpler for
agents — just pipe it:

  echo "$KEY" | onyx-cli configure --server-url https://...

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When running in a terminal, ask now prints dim status lines to stderr
as the backend processes the request:

  Searching documents...
    → what is our PTO policy
  Found 5 documents
  Thinking...

Status goes to stderr so it doesn't pollute stdout or interfere with
piping. Only shown when stdout is a TTY (agents don't see it).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/cmd/ask.go">

<violation number="1" location="cli/cmd/ask.go:133">
P2: `--quiet` mode still streams status updates to stderr; gate the new TTY progress prints behind `!askQuiet` so quiet mode remains non-streaming.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

rohoswagger and others added 3 commits April 2, 2026 18:24
Stdin piping still works implicitly (just pipe without any flag), but
--api-key-stdin makes the intent explicit and discoverable via --help.
Errors if used together with --api-key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TestConnection now returns *api.AuthError for 401/403 responses.
configure and validate-config check for this type and return
exitcodes.AuthFailure (4) instead of exitcodes.Unreachable (5),
so agents can distinguish "bad API key" from "server down".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Status updates (searching, thinking, tool use) were printing to stderr
even with --quiet. Now gated behind !askQuiet so quiet mode is truly
non-streaming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/internal/api/client.go">

<violation number="1" location="cli/internal/api/client.go:152">
P2: Reverse-proxy 401/403 errors are incorrectly classified as `AuthError`, causing auth-failure exit codes instead of unreachable/connectivity exit codes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if resp2.StatusCode == 401 || resp2.StatusCode == 403 {
if isHTML || strings.Contains(respServer, "awselb") {
return fmt.Errorf("HTTP %d from a reverse proxy (not the Onyx backend).\n Check your deployment's ingress / proxy configuration", resp2.StatusCode)
return &AuthError{Message: fmt.Sprintf("HTTP %d from a reverse proxy (not the Onyx backend).\n Check your deployment's ingress / proxy configuration", resp2.StatusCode)}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 3, 2026

Choose a reason for hiding this comment

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

P2: Reverse-proxy 401/403 errors are incorrectly classified as AuthError, causing auth-failure exit codes instead of unreachable/connectivity exit codes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/internal/api/client.go, line 152:

<comment>Reverse-proxy 401/403 errors are incorrectly classified as `AuthError`, causing auth-failure exit codes instead of unreachable/connectivity exit codes.</comment>

<file context>
@@ -149,12 +149,12 @@ func (c *Client) TestConnection(ctx context.Context) error {
 	if resp2.StatusCode == 401 || resp2.StatusCode == 403 {
 		if isHTML || strings.Contains(respServer, "awselb") {
-			return fmt.Errorf("HTTP %d from a reverse proxy (not the Onyx backend).\n  Check your deployment's ingress / proxy configuration", resp2.StatusCode)
+			return &AuthError{Message: fmt.Sprintf("HTTP %d from a reverse proxy (not the Onyx backend).\n  Check your deployment's ingress / proxy configuration", resp2.StatusCode)}
 		}
 		if resp2.StatusCode == 401 {
</file context>
Suggested change
return &AuthError{Message: fmt.Sprintf("HTTP %d from a reverse proxy (not the Onyx backend).\n Check your deployment's ingress / proxy configuration", resp2.StatusCode)}
return fmt.Errorf("HTTP %d from a reverse proxy (not the Onyx backend).\n Check your deployment's ingress / proxy configuration", resp2.StatusCode)
Fix with Cubic

Comment on lines +100 to +104
fmt.Printf("\n\n--- response truncated (%d bytes total) ---\n", w.totalBytes)
fmt.Printf("Full response: %s\n", tmpPath)
fmt.Printf("Explore:\n")
fmt.Printf(" cat %s | grep \"<pattern>\"\n", tmpPath)
fmt.Printf(" cat %s | tail -50\n", tmpPath)
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.

P1 Truncation metadata written to stdout contaminates agent output

The truncation notice and exploration hints are printed with fmt.Printf to stdout. For the stated agent-friendly goal, this is self-defeating: an AI agent reading stdout captures both the response content and the metadata as a single stream, making it impossible to cleanly separate the actual answer from the "file was truncated" advisory.

All progress/status messages in the stream handler (SearchStartEvent, SearchQueriesEvent, etc.) already correctly use fmt.Fprintf(os.Stderr, ...). The truncation footer should follow the same pattern:

fmt.Fprintf(os.Stderr, "\n\n--- response truncated (%d bytes total) ---\n", w.totalBytes)
fmt.Fprintf(os.Stderr, "Full response: %s\n", tmpPath)
fmt.Fprintf(os.Stderr, "Explore:\n")
fmt.Fprintf(os.Stderr, "  cat %s | grep \"<pattern>\"\n", tmpPath)
fmt.Fprintf(os.Stderr, "  cat %s | tail -50\n", tmpPath)

With this change, stdout stays clean machine-readable text and stderr carries the advisory, which is consistent with standard Unix tool conventions and the rest of the PR.

Prompt To Fix With AI
This is a comment left during a code review.
Path: cli/internal/overflow/writer.go
Line: 100-104

Comment:
**Truncation metadata written to stdout contaminates agent output**

The truncation notice and exploration hints are printed with `fmt.Printf` to stdout. For the stated agent-friendly goal, this is self-defeating: an AI agent reading stdout captures both the response content and the metadata as a single stream, making it impossible to cleanly separate the actual answer from the "file was truncated" advisory.

All progress/status messages in the stream handler (`SearchStartEvent`, `SearchQueriesEvent`, etc.) already correctly use `fmt.Fprintf(os.Stderr, ...)`. The truncation footer should follow the same pattern:

```go
fmt.Fprintf(os.Stderr, "\n\n--- response truncated (%d bytes total) ---\n", w.totalBytes)
fmt.Fprintf(os.Stderr, "Full response: %s\n", tmpPath)
fmt.Fprintf(os.Stderr, "Explore:\n")
fmt.Fprintf(os.Stderr, "  cat %s | grep \"<pattern>\"\n", tmpPath)
fmt.Fprintf(os.Stderr, "  cat %s | tail -50\n", tmpPath)
```

With this change, `stdout` stays clean machine-readable text and `stderr` carries the advisory, which is consistent with standard Unix tool conventions and the rest of the PR.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +52 to 74
if (apiKey == "" && !term.IsTerminal(int(os.Stdin.Fd()))) || apiKeyStdin {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read API key from stdin: %w", err)
}
apiKey = strings.TrimSpace(string(data))
}

if serverURL != "" && apiKey != "" {
return configureNonInteractive(serverURL, apiKey, dryRun)
}

if dryRun {
return exitcodes.New(exitcodes.BadRequest, "--dry-run requires --server-url and --api-key")
}

if serverURL != "" || apiKey != "" {
return exitcodes.New(exitcodes.BadRequest, "both --server-url and --api-key are required for non-interactive setup\n Run 'onyx-cli configure' without flags for interactive setup")
}

cfg := config.Load()
onboarding.Run(&cfg)
return nil
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.

P1 Stdin consumed before interactive fallback makes onboarding wizard hang on EOF

When stdin is piped but neither --server-url nor --api-key is provided (e.g. echo "" | onyx-cli configure or cat /dev/null | onyx-cli configure), the logic at line 52 detects a non-TTY stdin and calls io.ReadAll(os.Stdin), consuming the entire stream. After trim, apiKey is "", and since both serverURL and apiKey are empty, none of the early-return guards fire and execution falls through to onboarding.Run(&cfg).

onboarding.Run drives an interactive prompt wizard that reads from stdin — but stdin is now at EOF. Depending on the prompt library, this either panics, loops infinitely reading empty input, or silently produces a corrupt config.

The simplest fix is to guard stdin reading behind the condition that serverURL is already provided (i.e., we're actually attempting a non-interactive flow):

// Only read API key from stdin when --server-url was also given or --api-key-stdin was explicit.
// Unconditionally consuming stdin breaks the interactive wizard if the user runs
// `echo "" | onyx-cli configure` without flags.
if apiKeyStdin || (apiKey == "" && serverURL != "" && !term.IsTerminal(int(os.Stdin.Fd()))) {
    data, err := io.ReadAll(os.Stdin)
    ...
}

This ensures the interactive path always has a live stdin.

Prompt To Fix With AI
This is a comment left during a code review.
Path: cli/cmd/configure.go
Line: 52-74

Comment:
**Stdin consumed before interactive fallback makes onboarding wizard hang on EOF**

When stdin is piped but neither `--server-url` nor `--api-key` is provided (e.g. `echo "" | onyx-cli configure` or `cat /dev/null | onyx-cli configure`), the logic at line 52 detects a non-TTY stdin and calls `io.ReadAll(os.Stdin)`, consuming the entire stream. After trim, `apiKey` is `""`, and since both `serverURL` and `apiKey` are empty, none of the early-return guards fire and execution falls through to `onboarding.Run(&cfg)`.

`onboarding.Run` drives an interactive prompt wizard that reads from stdin — but stdin is now at EOF. Depending on the prompt library, this either panics, loops infinitely reading empty input, or silently produces a corrupt config.

The simplest fix is to guard stdin reading behind the condition that `serverURL` is already provided (i.e., we're actually attempting a non-interactive flow):

```go
// Only read API key from stdin when --server-url was also given or --api-key-stdin was explicit.
// Unconditionally consuming stdin breaks the interactive wizard if the user runs
// `echo "" | onyx-cli configure` without flags.
if apiKeyStdin || (apiKey == "" && serverURL != "" && !term.IsTerminal(int(os.Stdin.Fd()))) {
    data, err := io.ReadAll(os.Stdin)
    ...
}
```

This ensures the interactive path always has a live stdin.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to 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.

2 participants