Prettifying Cursor CLI Agent's Stream Format

Oct 13, 2025

Using Cursor from the CLI is a handy way to automate some tasks. For example, I have an script that detects changes since the readme was last updated and uses cursor to determine if any changes need to be made. The one bugbear of mine is that the command just sits there saying nothing until it's complete even with --output-format text.

It can stream messages as JSON which is better but hard to read:

{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Use this command to find files changed since the last README update:\n`git diff --find-renames --find-copies -U0 \"$(git log -n1 --format=%H -- README.md)..HEAD\"`\n\nReview the changed files list. Determine if any changes warrant README updates (new features, API changes, setup instructions, breaking changes). Ignore: refactors, internal changes, minor fixes.\n\nFor relevant changes only:\n- Use git diff to get specific file diffs if needed\n- Read/find files to understand context\n- Update README with minimal, precise changes\n- Keep existing structure and style\n- No fluff or rewrites\n\nIf no updates needed, say so and stop."}]},"session_id":"e7012a18-f795-4da2-973e-fa380290c445"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"I'll help you check for changes that might warrant README updates. Let me start by running the command to find files changed since the last README update.\n"}]},"session_id":"e7012a18-f795-4da2-973e-fa380290c445"}

With the help of jq, we can get a much nicer format:

output

Usage

cursor-agent -p --output-format stream-json "tell me about this repo"  | jq -f format-chat.jq

You can also create an alias in your shellrc to make it even easier:

pretty-cursor() {
  cursor-agent --output-format stream-json "$@" | jq -f format-chat.jq
}

For fish shell:

function pretty-cursor --wraps cursor-agent
  cursor-agent --output-format stream-json $argv | jq -f format-chat.jq
end

JQ Script

You can also make this script executable with chmod +x format-chat.jq so you can run it directly.

#!/usr/bin/env -S jq -r -f

# Helper function to truncate long strings
def truncate_string(str; max_length):
  if str | length > max_length then
    str[0:max_length] + "..."
  else
    str
  end;

# Helper function to format arguments with truncation
def format_args(args):
  if args == null then
    ""
  else
    " " + (args | to_entries | map(
      if .value | type == "string" then
        "\(.key): \"\(.value | truncate_string(50))\""
      elif .value | type == "array" then
        "\(.key): [\(.value | length) items]"
      elif .value | type == "object" then
        "\(.key): {\(.value | keys | length) fields}"
      else
        "\(.key): \(.value)"
      end
    ) | join(", "))
  end;

if .type == "user" then
  "\n\u001b[36m[USER]\u001b[0m\n\(.message.content[0].text)"
elif .type == "assistant" then
  "\n\u001b[32m[ASSISTANT]\u001b[0m \(.message.content[0].text)"
elif .type == "tool_call" and .subtype == "started" then
  if .tool_call.shellToolCall then
    "\n\u001b[33m[SHELL]\u001b[0m \(.tool_call.shellToolCall.args.command)"
  elif .tool_call.readToolCall then
    "\n\u001b[33m[READ]\u001b[0m \(.tool_call.readToolCall.args.path)\(if .tool_call.readToolCall.args.offset then " (offset: \(.tool_call.readToolCall.args.offset), limit: \(.tool_call.readToolCall.args.limit))" else "" end)"
  elif .tool_call.editToolCall then
    "\n\u001b[33m[EDIT]\u001b[0m \(.tool_call.editToolCall.args.path)"
  elif .tool_call.grepToolCall then
    "\n\u001b[33m[GREP]\u001b[0m \(.tool_call.grepToolCall.args.pattern) in \(.tool_call.grepToolCall.args.path)"
  elif .tool_call.lsToolCall then
    "\n\u001b[33m[LS]\u001b[0m \(.tool_call.lsToolCall.args.path)\(if .tool_call.lsToolCall.args.ignore and (.tool_call.lsToolCall.args.ignore | length) > 0 then " (ignore: \(.tool_call.lsToolCall.args.ignore | join(", ")))" else "" end)"
  elif .tool_call.globToolCall then
    "\n\u001b[33m[GLOB]\u001b[0m \(.tool_call.globToolCall.args.globPattern) in \(.tool_call.globToolCall.args.targetDirectory)"
  elif .tool_call.todoToolCall then
    "\n\u001b[33m[TODO]\u001b[0m \(.tool_call.todoToolCall.args.merge // false | if . then "merge" else "create" end) \(.tool_call.todoToolCall.args.todos | length) todos" + 
    (if .tool_call.todoToolCall.args.todos and (.tool_call.todoToolCall.args.todos | length) > 0 then
      "\n  " + (.tool_call.todoToolCall.args.todos | map(
        (.status // "unknown" | if . == "TODO_STATUS_PENDING" then "" elif . == "TODO_STATUS_IN_PROGRESS" then "🔄" elif . == "TODO_STATUS_COMPLETED" then "" elif . == "TODO_STATUS_CANCELLED" then "" else "" end) + " " + .content
      ) | join("\n  "))
    else "" end)
  elif .tool_call.updateTodosToolCall then
    "\n\u001b[33m[UPDATE_TODOS]\u001b[0m \(.tool_call.updateTodosToolCall.args.merge // false | if . then "merge" else "create" end) \(.tool_call.updateTodosToolCall.args.todos | length) todos" + 
    (if .tool_call.updateTodosToolCall.args.todos and (.tool_call.updateTodosToolCall.args.todos | length) > 0 then
      "\n  " + (.tool_call.updateTodosToolCall.args.todos | map(
        (.status // "unknown" | if . == "TODO_STATUS_PENDING" then "" elif . == "TODO_STATUS_IN_PROGRESS" then "🔄" elif . == "TODO_STATUS_COMPLETED" then "" elif . == "TODO_STATUS_CANCELLED" then "" else "" end) + " " + .content
      ) | join("\n  "))
    else "" end)
  elif .tool_call.writeToolCall then
    "\n\u001b[33m[WRITE]\u001b[0m \(.tool_call.writeToolCall.args.path) (\(.tool_call.writeToolCall.args.fileText | length) chars)\n  " + (.tool_call.writeToolCall.args.fileText | if length > 100 then .[0:100] + "..." else . end)
  elif .tool_call.deleteToolCall then
    "\n\u001b[33m[DELETE]\u001b[0m \(.tool_call.deleteToolCall.args.path)"
  else
    "\n\u001b[33m[TOOL]\u001b[0m \(.tool_call | keys[0])" + 
    (if .tool_call | to_entries[0].value.args then
      " " + (.tool_call | to_entries[0].value.args | to_entries | map(
        if .value | type == "string" then
          "\(.key): \"\(.value | if length > 50 then .[0:50] + "..." else . end)\""
        elif .value | type == "array" then
          "\(.key): [\(.value | length) items]"
        elif .value | type == "object" then
          "\(.key): {\(.value | keys | length) fields}"
        else
          "\(.key): \(.value)"
        end
      ) | join(", "))
    else "" end)
  end
elif .type == "tool_call" and .subtype == "completed" then
  if .tool_call.shellToolCall then
    if .tool_call.shellToolCall.result.success then
      "\n\u001b[90m✓ Exit \(.tool_call.shellToolCall.result.success.exitCode)\u001b[0m"
    else
      "\n\u001b[91m✗ Failed\u001b[0m"
    end
  elif .tool_call.readToolCall then
    if .tool_call.readToolCall.result.success then
      "\n\u001b[90m✓ Read \(.tool_call.readToolCall.result.success.totalLines) lines\u001b[0m"
    else
      "\n\u001b[91m✗ Read failed\u001b[0m"
    end
  elif .tool_call.editToolCall then
    if .tool_call.editToolCall.result.success then
      "\n\u001b[90m✓ Edited\u001b[0m"
    else
      "\n\u001b[91m✗ Edit failed\u001b[0m"
    end
  elif .tool_call.grepToolCall then
    if .tool_call.grepToolCall.result.success then
      "\n\u001b[90m✓ Found \(.tool_call.grepToolCall.result.success.workspaceResults | to_entries[0].value.content.totalMatchedLines) matches\u001b[0m"
    else
      "\n\u001b[91m✗ Grep failed\u001b[0m"
    end
  elif .tool_call.lsToolCall then
    if .tool_call.lsToolCall.result.success then
      "\n\u001b[90m✓ Listed \(.tool_call.lsToolCall.result.success.directoryTreeRoot.childrenFiles | length) files, \(.tool_call.lsToolCall.result.success.directoryTreeRoot.childrenDirs | length) dirs\u001b[0m"
    else
      "\n\u001b[91m✗ List failed\u001b[0m"
    end
  elif .tool_call.globToolCall then
    if .tool_call.globToolCall.result.success then
      "\n\u001b[90m✓ Found \(.tool_call.globToolCall.result.success.totalFiles) files\u001b[0m"
    else
      "\n\u001b[91m✗ Glob failed\u001b[0m"
    end
  elif .tool_call.todoToolCall then
    if .tool_call.todoToolCall.result.success then
      "\n\u001b[90m✓ Updated todos\u001b[0m" + 
      (if .tool_call.todoToolCall.result.success.todos and (.tool_call.todoToolCall.result.success.todos | length) > 0 then
        "\n  " + (.tool_call.todoToolCall.result.success.todos | map(
          (.status // "unknown" | if . == "TODO_STATUS_PENDING" then "" elif . == "TODO_STATUS_IN_PROGRESS" then "🔄" elif . == "TODO_STATUS_COMPLETED" then "" elif . == "TODO_STATUS_CANCELLED" then "" else "" end) + " " + .content
        ) | join("\n  "))
      else "" end)
    else
      "\n\u001b[91m✗ Todo update failed\u001b[0m"
    end
  elif .tool_call.updateTodosToolCall then
    if .tool_call.updateTodosToolCall.result.success then
      "\n\u001b[90m✓ Updated todos\u001b[0m" + 
      (if .tool_call.updateTodosToolCall.result.success.todos and (.tool_call.updateTodosToolCall.result.success.todos | length) > 0 then
        "\n  " + (.tool_call.updateTodosToolCall.result.success.todos | map(
          (.status // "unknown" | if . == "TODO_STATUS_PENDING" then "" elif . == "TODO_STATUS_IN_PROGRESS" then "🔄" elif . == "TODO_STATUS_COMPLETED" then "" elif . == "TODO_STATUS_CANCELLED" then "" else "" end) + " " + .content
        ) | join("\n  "))
      else "" end)
    else
      "\n\u001b[91m✗ Todo update failed\u001b[0m"
    end
  elif .tool_call.writeToolCall then
    if .tool_call.writeToolCall.result.success then
      "\n\u001b[90m✓ Wrote \(.tool_call.writeToolCall.result.success.linesCreated) lines (\(.tool_call.writeToolCall.result.success.fileSize) bytes) to \(.tool_call.writeToolCall.args.path)\u001b[0m"
    else
      "\n\u001b[91m✗ Write failed\u001b[0m"
    end
  elif .tool_call.deleteToolCall then
    if .tool_call.deleteToolCall.result.success then
      "\n\u001b[90m✓ Deleted \(.tool_call.deleteToolCall.args.path)\u001b[0m"
    elif .tool_call.deleteToolCall.result.rejected then
      "\n\u001b[91m✗ Delete rejected: \(.tool_call.deleteToolCall.result.rejected.reason // "unknown reason")\u001b[0m"
    else
      "\n\u001b[91m✗ Delete failed\u001b[0m"
    end
  else
    "\n\u001b[90m✓ Completed\u001b[0m"
  end
elif .type == "result" then
  "\n\u001b[35m[RESULT]\u001b[0m \(.subtype) (\(.duration_ms)ms)"
else
  empty
end

Update readme prompt

Since I mentioned the update readme script in this post, I thought I'd share the prompt I use for it:

Use this command to find files changed since the last README update:
`git diff --find-renames --find-copies -U0 "$(git log -n1 --format=%H -- README.md)..HEAD"`

Review the changed files list. Determine if any changes warrant README updates (new features, API changes, setup instructions, breaking changes). Ignore: refactors, internal changes, minor fixes.

For relevant changes only:
- Use git diff to get specific file diffs if needed
- Read/find files to understand context
- Update README with minimal, precise changes
- Keep existing structure and style
- No fluff or rewrites

If no updates needed, say so and stop.

I use this with a fish function that will load prompts from files in ~/.prompts. So I can run cursh update-readme to update the readme. It registers an autocomplete function for the prompt names so I can tab complete them.

function cursh
    # Check if at least one argument is provided
    if test (count $argv) -lt 1
        echo "Usage: cursh <prompt_name> [additional_args...]"
        echo "Loads prompt from ~/.prompts/<prompt_name>.md and passes remaining args to pretty-cursor"
        return 1
    end

    # Get the first argument as the prompt name
    set prompt_name $argv[1]
    set prompt_file ~/.prompts/$prompt_name.md
    
    # Check if the prompt file exists
    if not test -f $prompt_file
        echo "Error: Prompt file $prompt_file not found"
        return 1
    end
    
    # Read the prompt file content
    set prompt_content (cat $prompt_file)
    
    # Get remaining arguments (everything after the first argument)
    set remaining_args $argv[2..-1]
    
    # Call pretty-cursor with the prompt content and remaining arguments
    echo $prompt_content | pretty-cursor $remaining_args
end

# Autocomplete function for prompt names
function __cursh_complete_prompts
    # List all .md files in ~/.prompts/ directory
    if test -d ~/.prompts
        for file in ~/.prompts/*.md
            # Extract just the filename without path and extension
            basename (string replace -r '\.md$' '' $file)
        end
    end
end

# Register the autocomplete
complete -c cursh -a '(__cursh_complete_prompts)' -d 'Prompt name (without .md extension)'
RSS
https://tarq.net/posts/atom.xml