Optimize CLI for AI agents in non-interactive mode#118
Merged
joetannenbaum merged 29 commits intomainfrom Apr 3, 2026
Merged
Conversation
confirm() defaults to true in Laravel Prompts when STDIN is not a TTY. Without `default: false`, instance:delete would proceed with deletion in non-interactive mode even without --force.
…terns The $dontConfirm pattern required both --force AND a positional argument, meaning --force alone (with resolver) still triggered confirm() which silently returns true in non-interactive mode. Simplified to the standard delete confirmation pattern with default: false.
Agents parsing stdout for valid JSON responses would confuse error messages with success output. Errors now go to stderr and include an 'error' key for easy identification.
Keeps stdout clean for valid JSON responses. Error output now includes an 'error' key for easy identification by agents.
The command already had JSON output logic but no explicit --json flag, forcing agents to use --no-interaction instead.
New BaseCommand::writeJsonIfWanted() writes JSON to stdout without exiting, unlike outputJsonIfWanted. Used in deploy to emit an initial 'initiated' JSON line with the deployment_id before the monitoring loop starts.
Added a data array parameter to ensureInteractive() that gets merged into the JSON error output. All resolvers now pass their available options, so agents get clean structured JSON with an 'options' key instead of having to make a separate list call.
10 delete commands were missing --json and produced no output in non-interactive mode. Now all delete commands support --json and output a confirmation message on success.
Non-interactive agents now get a JSON object with deployment_id, status, failure_reason, and a hint about recovery commands instead of just an error message.
ValidationErrors::toJson() now includes error and message keys matching the schema used by all other error output. Also writes to stderr instead of stdout in breakValidationLoopIfNonInteractive.
Allows agents to initiate a deployment and return immediately with the deployment ID. They can then poll with deployment:get --json.
All resolver failAndExit messages now tell agents which list command to run to discover available resources.
confirm() calls would silently create directories and write files in non-interactive mode. Now bails early with a hint to use --print.
OutputStyle::getErrorOutput() is protected and not callable from outside the class. Use fwrite(STDERR) consistently with the resolvers.
The command previously funneled entirely into an interactive keyboard- driven prompt, making it useless for agents. Now polls and outputs JSON status lines in non-interactive mode.
The agent-detector package returns 'cursor' not 'cursor-cli' for the Cursor enum value.
When not authenticated, password() silently returns empty in non- interactive mode. When multiple tokens exist, select() picks the default silently. Now throws with actionable error messages instead.
HasAClient is used by both BaseCommand and RequiresAuthToken middleware. The middleware doesn't have isInteractive(), so use stream_isatty(STDIN) directly to detect non-interactive mode.
stream_isatty alone misses agent detection and CI environment variables. Now also checks isNonInteractiveEnvironment() which covers agents, GitHub Actions, GitLab CI, Jenkins, etc.
…into non-interactive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
confirm()in Laravel Prompts defaults totruewhen STDIN is not a TTY, causinginstance:delete,database-cluster:delete, andbackground-process:deleteto silently proceed without confirmation in non-interactive mode{"error": true, "message": "..."}schema so agents can cleanly separate errors from valid responses--jsonto deploy and all delete commands: 10 delete commands and the deploy command were missing--jsonflags, producing no output for agentsensureInteractive()now includes available options as structured JSON data, andfailAndExit()messages include actionable hints (e.g. "Runcloud application:list --json")--no-waitflag to initiate and return immediatelycompletionscommand from silently creating directories/files in non-interactive modeValidationErrors::toJson()now matches the standard error format