Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,21 @@ jobs:
config_dir="$(mktemp -d)"
trap 'rm -rf "$config_dir"' EXIT

test "$(DD_ACCESS_TOKEN="$expected_token" "$bin" --no-agent auth token)" = "$expected_token"
"$bin" --no-agent auth --help >"$config_dir/help"
test "$(DD_ACCESS_TOKEN="$expected_token" "$bin" auth token)" = "$expected_token"
"$bin" auth --help >"$config_dir/help"
grep -q '^ token' "$config_dir/help"
"$bin" completions zsh >"$config_dir/completions"
grep -q 'token:Print the current OAuth access token for credential-command integrations' "$config_dir/completions"
PUP_CONFIG_DIR="$config_dir" DD_TOKEN_STORAGE=file \
"$bin" --agent agent schema >"$config_dir/full-schema"
"$bin" agent schema >"$config_dir/full-schema"
! grep -q '"full_path": "auth token"' "$config_dir/full-schema"
PUP_CONFIG_DIR="$config_dir" DD_TOKEN_STORAGE=file \
"$bin" --agent agent schema --compact >"$config_dir/compact-schema"
"$bin" agent schema --compact >"$config_dir/compact-schema"
! grep -q '"full_path": "auth token"' "$config_dir/compact-schema"

if env -u DD_ACCESS_TOKEN -u DD_API_KEY -u DD_APP_KEY \
PUP_CONFIG_DIR="$config_dir" DD_TOKEN_STORAGE=file \
"$bin" --no-agent auth token >"$config_dir/stdout" 2>"$config_dir/stderr"; then
"$bin" auth token >"$config_dir/stdout" 2>"$config_dir/stderr"; then
echo "ERROR: auth token succeeded without credentials"
exit 1
fi
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ pup/
│ ├── client.rs # Datadog API client wrapper
│ ├── config.rs # Generator-owned configuration contract
│ ├── formatter.rs # Generator-owned formatting contract
│ ├── output.rs # Output rendering and printing (JSON, YAML, table, agent envelope)
│ ├── output.rs # Output rendering and printing (JSON envelope, YAML, table, CSV, TSV)
│ ├── settings.rs # Configuration loading, precedence, and storage paths
│ ├── useragent.rs # AI agent detection (FORCE_AGENT_MODE, Claude Code, etc.)
│ ├── useragent.rs # AI agent detection (User-Agent token, prompt auto-approval)
│ ├── util.rs # Time parsing, validation
│ └── version.rs # Version and build info
├── tests/ # E2E parity report
Expand Down
87 changes: 45 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,51 +486,54 @@ pup skills install --name dd-idp
- `DD_AUTO_APPROVE`: Auto-approve destructive operations (true/false)
- `DD_TOKEN_STORAGE`: Token storage backend (`keychain` (default) or `file`). Can also be set as `token_storage` in the config file.

## Agent Mode

When pup is invoked by an AI coding agent, it automatically switches to **agent mode** which returns structured JSON responses optimized for machine consumption (including metadata, error details, and hints). Agent mode also auto-approves confirmation prompts.

Agent mode is **auto-detected** when any of these environment variables is set to `1` or `true`:

| Variable | Agent |
|----------|-------|
| `CLAUDE_CODE` or `CLAUDECODE` | Claude Code |
| `CURSOR_AGENT` | Cursor |
| `CODEX` or `OPENAI_CODEX` | OpenAI Codex |
| `AIDER` | Aider |
| `CLINE` | Cline |
| `WINDSURF_AGENT` | Windsurf |
| `GITHUB_COPILOT` or `COPILOT_CLI` | GitHub Copilot |
| `AMAZON_Q` or `AWS_Q_DEVELOPER` | Amazon Q |
| `GEMINI_CODE_ASSIST` | Gemini Code Assist |
| `GEMINI_CLI` | Gemini CLI |
| `SRC_CODY` | Sourcegraph Cody |
| `PI_CODING_AGENT` | pi.dev |
| `FORCE_AGENT_MODE` | Any agent (manual override) |

It is also auto-detected when any of these harness-injected variables is present and non-empty:

| Variable | Agent |
|----------|-------|
| `CODEX_SESSION_ID`, `CODEX_THREAD_ID`, `CODEX_VERSION`, `CODEX_SANDBOX`, or `CODEX_CI` | OpenAI Codex |
| `COPILOT_AGENT_SESSION_ID` | GitHub Copilot |
| `CURSOR_TRACE_ID` | Cursor |
| `DEVIN_SESSION_ID` | Devin |

You can also enable it explicitly with the `--agent` flag or by setting `FORCE_AGENT_MODE=1`:
## AI Agents

```bash
# Auto-detected (e.g., running inside Claude Code)
pup monitors list
Pup behaves the same whether a person or an AI coding agent runs it: JSON output is always wrapped in the same envelope, and `--help` is always standard text help.

```json
{
"status": "success",
"data": [ ... ],
"meta": { "page": { "after": "eyJ..." } },
"metadata": {
"command": "logs search",
"count": 50,
"truncated": true,
"next_cursor": "eyJ...",
"next_action": "More results available. Use --cursor=\"eyJ...\" ..."
}
}
```

# Explicit flag
pup monitors list --agent
- `data` is the command payload. When the API response has a top-level `data`, it stays in place and the response's other keys (`meta`, `links`, `included`) are kept next to it; any other response becomes `data`.
- `metadata` is always present. `truncated` says whether pup knows more results exist, and `next_cursor` / `next_offset` say where the next page starts, even after `--jq`. `next_action` names the command's own cursor flag.
- `--jq` runs on the raw API response; its result becomes `data`.
- JSON and YAML output carry the envelope. Table, CSV, and TSV print only the result rows on stdout (so CSV/TSV stay parseable) and always print the metadata to stderr as one block:

```text
metadata: command="monitors search" count=30 truncated=true next_page=1
next_action: More results available; rerun with --page=1
warning: <only when there are warnings>
```

The `metadata:` line always appears, with `key=value` pairs in a fixed order (command, count, truncated, next_cursor, next_offset, next_page; absent keys omitted, strings JSON-quoted). With `--no-envelope`, only `warning:` lines are printed.
- `--no-envelope` (or `PUP_NO_ENVELOPE=1`) prints the raw API response instead. The legacy `--no-agent` flag does the same and also turns off agent auto-approval.

Pup detects common AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, and others) from their environment variables to add an `ai-agent <name>` token to the User-Agent header for telemetry and to auto-approve CLI prompts. Detection changes nothing else: output and help are the same as for humans.

For agents and scripts:

```bash
# Machine-readable command reference (JSON)
pup agent schema
pup agent schema --compact
pup agent schema logs aggregate # One command, including its response shape

# Environment variable override
FORCE_AGENT_MODE=1 pup monitors list
# Skip confirmation prompts for write operations
pup monitors delete 12345 --yes
```

If you are integrating pup into an AI agent workflow, make sure the appropriate environment variable is set so responses are optimized for your agent. Without it, pup defaults to human-friendly output.
When pup detects an AI agent, it auto-approves CLI prompts, as it does with `--yes` or `DD_AUTO_APPROVE=true`; pass `--no-agent` to keep the prompts. Agents should still get the user's explicit authorization before any remote change. The legacy `--agent` flag is accepted and ignored.

## WASM

Expand Down Expand Up @@ -609,8 +612,8 @@ pup runbooks validate ./my-runbook.yaml
- **HTTP steps**: Full method support (GET/POST/PUT/PATCH/DELETE) with `body`, `headers`, `content_type`, and `body_file`
- **Failure handling**: `on_failure: fail|warn|ignore` and `optional: true` per step
- **Conditional execution**: `when: on_success|on_failure|always`
- **Polling**: `poll.interval`, `poll.timeout`, `poll.until` for long-running operations
- **Output capture**: `capture: VAR_NAME` stores stdout for use in later steps
- **Polling**: `poll.interval`, `poll.timeout`, `poll.until` for long-running operations; `until` is evaluated against the raw payload (as `--no-envelope` prints it)
- **Output capture**: `capture: VAR_NAME` stores a pup step's raw payload (as `--no-envelope` prints it, in JSON or YAML) for use in later steps; the step's displayed output still follows your format and envelope choice
- **Timestamped output**: Every step shows start time, elapsed duration, and labeled stdout/stderr

See `docs/examples/runbooks/` for ready-to-use examples and [docs/EXAMPLES.md](docs/EXAMPLES.md) for full reference.
Expand Down
2 changes: 1 addition & 1 deletion agents/service-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The Software Catalog entity API and the IDP entity graph are related but distinc

## Write workflows

Pup agent mode may auto-approve CLI prompts. Obtain the user's explicit authorization immediately before any remote mutation even when Pup would not prompt.
Pup auto-approves CLI prompts when it detects an AI agent (and when `--yes` or `DD_AUTO_APPROVE` is set). Obtain the user's explicit authorization immediately before any remote mutation even when Pup would not prompt.

### Register Catalog entities

Expand Down
11 changes: 4 additions & 7 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Based on [RFC 6749](https://tools.ietf.org/html/rfc6749) and [RFC 7636](https://
**Fallback:**
- If refresh fails, prompt user to re-authenticate

## User Agent & Agent Mode
## User Agent

Custom user agent identifies pup CLI and detects AI coding assistants:

Expand All @@ -163,13 +163,10 @@ pup/v0.1.0 (rust; os darwin; arch arm64; ai-agent claude-code) # With agent

Table-driven registry detecting AI coding agents via environment variables. First match wins:
- Claude Code (`CLAUDECODE`, `CLAUDE_CODE`), Cursor (`CURSOR_AGENT`; or presence of `CURSOR_TRACE_ID`), Codex (`CODEX`, `OPENAI_CODEX`; or presence of `CODEX_SESSION_ID`/`CODEX_THREAD_ID`/`CODEX_VERSION`/`CODEX_SANDBOX`/`CODEX_CI`, which Codex injects into shell commands), OpenCode (`OPENCODE`), Aider (`AIDER`), Cline (`CLINE`), Windsurf (`WINDSURF_AGENT`), GitHub Copilot (`GITHUB_COPILOT`, `COPILOT_CLI`; or presence of `COPILOT_AGENT_SESSION_ID`), Amazon Q (`AMAZON_Q`, `AWS_Q_DEVELOPER`), Gemini Code Assist (`GEMINI_CODE_ASSIST`), Gemini CLI (`GEMINI_CLI`), Sourcegraph Cody (`SRC_CODY`), pi.dev (`PI_CODING_AGENT`), Devin (presence of `DEVIN_SESSION_ID`), Generic Agent (`AGENT`)
- Manual override: `FORCE_AGENT_MODE=1` or `--agent` flag

**Agent Mode Behavior** (when detected):
- `--help` returns structured JSON schema instead of text
- Confirmation prompts auto-approved (prevents stdin hangs)
- API responses wrapped in metadata envelope (count, truncation, warnings)
- Errors returned as structured JSON with suggestions
Detection adds the `ai-agent <name>` token to the User-Agent header and auto-approves CLI prompts (`cfg.auto_approve`, also set in the extension pre-parse path so extension children get `PUP_AUTO_APPROVE`). It does not change output, help, or colors: agents get the same JSON envelope and text help as humans. The legacy `--no-agent` flag opts out of agent auto-approval; `--agent` is a hidden no-op.

**Output envelope** (`src/output.rs`): JSON output is wrapped in `{status, data, metadata}` by default. A response with a top-level `data` keeps it, and its sibling keys, in place; any other response becomes `data`. `metadata` (count, truncated, command, next_cursor, next_offset, next_action, warnings) is derived from the response unless a command sets it, so continuation survives `--jq`. `--no-envelope` (or the legacy `--no-agent`, env `PUP_NO_ENVELOPE`) prints the raw response.

See [LLM_GUIDE.md](LLM_GUIDE.md) for the complete agent guide.

Expand Down
29 changes: 10 additions & 19 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ up to 39 decimal digits. To save the full response without reading it into agent
context, redirect stdout:

```bash
pup --agent --read-only traces get <TRACE_ID> > trace.json
pup --read-only traces get <TRACE_ID> > trace.json
```

In agent mode, spans are under `.data.attributes.spans`; check
Spans are under `.data.attributes.spans`; check
`.data.attributes.is_truncated` before treating the response as complete.
The command requests all available fields. Preserve integer ID precision when
parsing the saved JSON.
Expand Down Expand Up @@ -318,21 +318,12 @@ pup logs search --query="status:error" --jq '.data | length'
- 1 output → the value (unwrapped)
- 2+ outputs → an array

**Agent mode — filter target:** `--jq` runs on the **raw response payload**, which
is the value that appears under `.data` in agent mode. Write expressions against the
payload (e.g. `.[]`), **not** against the envelope (`.data[]` will not work):

```bash
# correct — targets the payload array
pup monitors list --agent --jq '.[0]'

# wrong — .data does not exist in the payload --jq sees
pup monitors list --agent --jq '.data[0]'
```

**Agent mode — metadata:** when `--jq` is active, `metadata.count` and
`metadata.truncated` are omitted from the envelope because they describe the
pre-filter data, not the filtered result.
**Filter target:** `--jq` runs on the **raw API response body** (what
`--no-envelope` prints). In JSON and YAML output its result becomes the envelope's
`data`, and `metadata` (including `truncated` and `next_cursor`) is kept, so a filter
never hides that more results exist. In table, CSV, and TSV output the jq result is
rendered as rows as-is, and the metadata goes to stderr. `pup agent schema <command>` reports the starting
expression for each command as `returns.jq_root`.

**Limitation:** commands that print output directly (e.g. `pup auth login`, some runbook
steps) bypass `format_and_print` and do not honor `--jq`.
Expand Down Expand Up @@ -373,8 +364,8 @@ Constraints worth knowing before relying on these:
- **No targeted edits.** `update` replaces and `edit` appends; there is no way to
modify one section in place.
- `--jq` is rejected with `--markdown`, since the output is not JSON.
- `--output` and agent mode have no effect under `--markdown`: the document is
printed as-is, with no envelope and no format conversion. This matches
- `--output` has no effect under `--markdown`: the document is printed as-is,
with no format conversion. This matches
`skills remote get`, the other command that emits raw Markdown.

### v1.13.x — Session Replay API Support (#182)
Expand Down
23 changes: 10 additions & 13 deletions docs/EXTENSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,15 @@ Extensions receive pup's auth credentials via environment variables. This means
| `DD_ORG` | Org is specified | Named org session |
| `PUP_OUTPUT` | Always | Output format (`json`, `table`, `yaml`, `csv`, `tsv`) |
| `PUP_FILTER` | `--jq` flag is set | The jq expression, verbatim |
| `PUP_AUTO_APPROVE` | `--yes` flag or agent mode | `true` |
| `PUP_AUTO_APPROVE` | `--yes` flag, `DD_AUTO_APPROVE`, or a detected AI agent | `true` |
| `PUP_READ_ONLY` | Read-only mode | `true` |
| `PUP_AGENT_MODE` | Agent mode | `true` |
| `PUP_NO_ENVELOPE` | `--no-envelope` flag or `PUP_NO_ENVELOPE` | `true` |

Pup refreshes the OAuth2 token if needed before passing it to the extension, so extensions always receive a valid token.

Variables not active in the current session are explicitly removed from the child environment to prevent stale credentials from leaking through the parent shell.

The `PUP_OUTPUT`, `PUP_FILTER`, `PUP_AGENT_MODE`, `PUP_READ_ONLY`, and `PUP_AUTO_APPROVE` variables are read back by a child `pup` process. So if your extension shells out to `pup` (see below), those nested calls automatically inherit the format, `--jq` filter, and mode the user selected on the parent command. An extension that prints its own JSON directly — rather than delegating to a nested `pup` call — still needs to read `PUP_FILTER` itself and apply the jq expression if it wants to honor `--jq`.
The `PUP_OUTPUT`, `PUP_FILTER`, `PUP_READ_ONLY`, `PUP_AUTO_APPROVE`, and `PUP_NO_ENVELOPE` variables are read back by a child `pup` process. So if your extension shells out to `pup` (see below), those nested calls automatically inherit the format, `--jq` filter, and mode the user selected on the parent command. An extension that prints its own JSON directly — rather than delegating to a nested `pup` call — still needs to read `PUP_FILTER` itself and apply the jq expression if it wants to honor `--jq`.

### Example: using auth in a Python extension

Expand Down Expand Up @@ -155,16 +155,16 @@ The examples above hand-roll HTTP and auth. You usually don't need to. Because p

```bash
#!/bin/bash
# GET as JSON, then extract fields. --silent suppresses pup's own rendering.
pup api v2/monitors --silent | jq -r '.[].name'
# GET the raw response body, then extract fields.
pup api v1/monitor --no-envelope | jq -r '.[].name'

# POST a typed body (-F coerces ints/bools/null; -f keeps raw strings).
pup api v2/tags/hosts/myhost -X POST -F source=web
```

### Render output with `pup format`

`pup format` (alias `fmt`) reads a JSON document from stdin (or `--input FILE`) and prints it using the caller's output format and agent mode — the same JSON / YAML / table / CSV / TSV rendering and agent envelope every built-in command uses. Because pup forwards `PUP_OUTPUT` and a child `pup` reads it back, your extension inherits the format the user originally requested.
`pup format` (alias `fmt`) reads a JSON document from stdin (or `--input FILE`) and prints it using the caller's output format — the same JSON envelope and YAML / table / CSV / TSV rendering every built-in command uses. Input that is already a pup envelope (for example `pup api` output) is re-rendered rather than wrapped twice, and its metadata is kept. `--count`, `--command`, and `--next-action` override the envelope's `metadata` fields. Because pup forwards `PUP_OUTPUT` and a child `pup` reads it back, your extension inherits the format the user originally requested.

```bash
#!/bin/bash
Expand All @@ -175,16 +175,14 @@ echo "$results" | pup format

# Or force a specific format.
echo "$results" | pup format --output table

# Populate the agent-mode envelope metadata.
echo "$results" | pup format --count 2 --command "foo list"
```

Extensions can also tell the table formatter where the result rows live and which
columns are useful. `--rows-at` accepts an RFC 6901 JSON Pointer into the response,
and `--row-at` optionally selects a value within every result row. `--columns`
accepts a comma-separated or repeated list of flattened field names. These hints
only affect table output; JSON, YAML, CSV, and TSV retain the complete response.
select the rows and columns for table, CSV, and TSV output; JSON and YAML retain the
complete response.

```bash
pup api v1/monitor/search --silent |
Expand All @@ -200,12 +198,12 @@ A fully consistent extension that adds zero auth or formatting code:

```bash
#!/bin/bash
pup api v2/monitors --silent | pup format
pup api v2/monitors | pup format
```

## Global Flags

Pup's global flags (`--output`, `--yes`, `--agent`, `--read-only`, `--org`) are parsed by pup before dispatching to the extension. They are NOT passed as CLI arguments to the extension - instead, they are forwarded as environment variables (see the table above).
Pup's global flags (`--output`, `--yes`, `--read-only`, `--org`) are parsed by pup before dispatching to the extension. They are NOT passed as CLI arguments to the extension - instead, they are forwarded as environment variables (see the table above).

```bash
# --output table is consumed by pup, extension receives PUP_OUTPUT=table
Expand Down Expand Up @@ -484,5 +482,4 @@ To extract an existing pup feature into an extension:
## Limitations

- **Source must be a regular file**: `pup extension install --local` requires the source path to be a regular file, not a directory.
- **Agent-mode help**: `pup --agent <ext-name> --help` prints pup's top-level schema, not the extension's help. In normal mode, `--help` is passed through to the extension.
- **No signing**: Downloaded binaries are not signed. If a release includes `checksums.txt`, pup verifies the selected archive checksum before installing. Only install extensions from trusted sources.
Loading
Loading