Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,8 @@ The usual flow is plural to find an ID, then singular with that ID. The split ke

`gateway_events_read` and `gateway_requests_read` **list** actions support the same filters as `hookdeck gateway event list` and `hookdeck gateway request list` — including payload search (`body`, `headers`, `parsed_query`, `path`) and date windows via `*_after` / `*_before` (ISO 8601; maps to API `field[gte]` / `field[lte]`). See `gateway_help` with topic `gateway_events_read` or `gateway_requests_read` for the full parameter list.

Both **list** actions also take `include: "data"`, which returns each event's or request's payload (headers, body, path, parsed query) in the list itself, so an agent that needs several payloads makes one call instead of one per record. Payloads make responses much larger, so pair it with a small `limit` and page with `next`. When `include` is set without a `limit`, the tool sends `limit=10` rather than the API's default page size. `list_ignored` does not take it: its route does not declare `include`. On the command line, the same option is `--include data` on `gateway event list`, `gateway request list` and `gateway request events`; it requires `--output json`, and the limit defaults to 10 there too.

The only relationship traversal the API supports is request → events, and `gateway_events_read` owns both directions of it. Pass `request_id` with action `list` for the events a request produced, or action `list_ignored` for the ones a connection filter dropped. `GET /events` declares no `request_id` filter, so the tool queries the request's own events route instead — it takes the same filters, so every argument still applies (`list_ignored`'s route is the exception: it takes `id`, paging and ordering only). There is no `event_id` filter on requests. To go the other way, read `request_id` off an event and call `gateway_request_read` with action `get`.

`gateway_help` reports which mode the session is in and lists only the actions it can perform.
Expand Down
18 changes: 18 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1593,6 +1593,9 @@ List events (processed webhook deliveries). Filter by connection ID, source, des
Use `--search-term` to find a complete value across the body, headers, parsed query or path
at once, when you know the value but not which field carries it.

Use `--include`=data with `--output`=json to get each event's payload in the results, instead of
fetching events one at a time. Without `--limit`, the limit is then 10 instead of 100.

**Usage:**

```bash
Expand All @@ -1615,6 +1618,7 @@ hookdeck gateway event list [flags]
| `--error-code` | `string` | Filter by error code |
| `--headers` | `string` | Filter by headers (JSON string) |
| `--id` | `string` | Filter by event ID(s) (comma-separated) |
| `--include` | `string` | Set to "data" to include each event's payload (headers, body, path, parsed query) in the results. Requires `--output` json. Responses grow with every payload: when `--limit` is not set it defaults to 10 instead of 100 |
| `--issue-id` | `string` | Filter by issue ID |
| `--last-attempt-at-after` | `string` | Filter by last_attempt_at after (ISO date-time) |
| `--last-attempt-at-before` | `string` | Filter by last_attempt_at before (ISO date-time) |
Expand Down Expand Up @@ -1642,6 +1646,7 @@ hookdeck gateway event list --connection-id web_abc123
hookdeck gateway event list --status FAILED --limit 20
hookdeck gateway event list --search-term cus_1234
hookdeck gateway event list --status QUEUED --next-attempt-at-before 2026-01-01T00:00:00Z
hookdeck gateway event list --status FAILED --include data --output json
```
### hookdeck gateway event get

Expand Down Expand Up @@ -1743,6 +1748,9 @@ Use `--search-term` to find a complete value across the body, headers, parsed qu
at once. `--events-count` 0 finds requests that produced no events, which is the usual reason a
webhook appears to have gone missing.

Use `--include`=data with `--output`=json to get each request's payload in the results, instead of
fetching requests one at a time. Without `--limit`, the limit is then 10 instead of 100.

**Usage:**

```bash
Expand All @@ -1762,6 +1770,7 @@ hookdeck gateway request list [flags]
| `--headers` | `string` | Filter by headers (JSON string) |
| `--id` | `string` | Filter by request ID(s) (comma-separated) |
| `--ignored-count` | `string` | Filter by number of ignored events. A whole number |
| `--include` | `string` | Set to "data" to include each request's payload (headers, body, path, parsed query) in the results. Requires `--output` json. Responses grow with every payload: when `--limit` is not set it defaults to 10 instead of 100 |
| `--ingested-at-after` | `string` | Filter by ingested_at after (ISO date-time) |
| `--ingested-at-before` | `string` | Filter by ingested_at before (ISO date-time) |
| `--limit` | `int` | Limit number of results (default "100") |
Expand All @@ -1784,6 +1793,7 @@ hookdeck gateway request list
hookdeck gateway request list --source-id src_abc123 --limit 20
hookdeck gateway request list --search-term cus_1234
hookdeck gateway request list --events-count 0
hookdeck gateway request list --source-id src_abc123 --include data --output json
```
### hookdeck gateway request get

Expand Down Expand Up @@ -1835,6 +1845,12 @@ List events (deliveries) created from a request.
Filters match `hookdeck gateway event list`: this command queries the same event
collection, narrowed to one request.

Use `--include`=data with `--output`=json to get each event's payload in the results. Without
`--limit`, the limit is then 10 instead of 100.

When the request has more events than fit on one page, a note on stderr gives the cursor to
pass to `--next` for the following page.

**Usage:**

```bash
Expand All @@ -1856,6 +1872,7 @@ hookdeck gateway request events <request-id> [flags]
| `--dir` | `string` | Sort direction (asc, desc) |
| `--error-code` | `string` | Filter by error code |
| `--headers` | `string` | Filter by headers (JSON string) |
| `--include` | `string` | Set to "data" to include each event's payload (headers, body, path, parsed query) in the results. Requires `--output` json. Responses grow with every payload: when `--limit` is not set it defaults to 10 instead of 100 |
| `--issue-id` | `string` | Filter by issue ID |
| `--last-attempt-at-after` | `string` | Filter by last_attempt_at after (ISO date-time) |
| `--last-attempt-at-before` | `string` | Filter by last_attempt_at before (ISO date-time) |
Expand All @@ -1881,6 +1898,7 @@ hookdeck gateway request events <request-id> [flags]
hookdeck gateway request events req_abc123
hookdeck gateway request events req_abc123 --status FAILED
hookdeck gateway request events req_abc123 --destination-id des_abc123
hookdeck gateway request events req_abc123 --include data --output json
```
### hookdeck gateway request ignored-events

Expand Down
14 changes: 12 additions & 2 deletions pkg/cmd/event_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type eventListCmd struct {
path string
parsedQuery string
searchTerm string
include string
orderBy string
dir string
limit int
Expand All @@ -59,12 +60,16 @@ func newEventListCmd() *eventListCmd {
Use --search-term to find a complete value across the body, headers, parsed query or path
at once, when you know the value but not which field carries it.

Use --include=data with --output=json to get each event's payload in the results, instead of
fetching events one at a time. Without --limit, the limit is then 10 instead of 100.

Examples:
hookdeck gateway event list
hookdeck gateway event list --connection-id web_abc123
hookdeck gateway event list --status FAILED --limit 20
hookdeck gateway event list --search-term cus_1234
hookdeck gateway event list --status QUEUED --next-attempt-at-before 2026-01-01T00:00:00Z`,
hookdeck gateway event list --status QUEUED --next-attempt-at-before 2026-01-01T00:00:00Z
hookdeck gateway event list --status FAILED --include data --output json`,
RunE: ec.runEventListCmd,
}

Expand Down Expand Up @@ -92,6 +97,7 @@ Examples:
ec.cmd.Flags().StringVar(&ec.path, "path", "", "Filter by path")
ec.cmd.Flags().StringVar(&ec.parsedQuery, "parsed-query", "", "Filter by parsed query (JSON string)")
ec.cmd.Flags().StringVar(&ec.searchTerm, "search-term", "", "Match a whole value in body, headers, parsed query or path. Not a substring (min 3 characters)")
ec.cmd.Flags().StringVar(&ec.include, "include", "", includeFlagUsage("event"))
ec.cmd.Flags().StringVar(&ec.orderBy, "order-by", "", "Sort key (e.g. created_at)")
ec.cmd.Flags().StringVar(&ec.dir, "dir", "", "Sort direction (asc, desc)")
ec.cmd.Flags().IntVar(&ec.limit, "limit", 100, "Limit number of results")
Expand Down Expand Up @@ -197,7 +203,11 @@ func (ec *eventListCmd) runEventListCmd(cmd *cobra.Command, args []string) error
if ec.dir != "" {
params["dir"] = ec.dir
}
params["limit"] = strconv.Itoa(ec.limit)
limit, err := applyIncludeData(cmd, ec.include, ec.output, ec.limit, params)
if err != nil {
return err
}
params["limit"] = strconv.Itoa(limit)
if ec.next != "" {
params["next"] = ec.next
}
Expand Down
56 changes: 51 additions & 5 deletions pkg/cmd/gateway_list_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ import (
// filter, because the answer looks right.
func captureListQuery(t *testing.T, args ...string) url.Values {
t.Helper()
got, err := runListQuery(t, args...)
require.NoError(t, err)
require.NotNil(t, got, "the command made no list request")
return got
}

// runListQuery is captureListQuery for a command that may fail: it returns the
// error and the query sent, which is nil when no list request was made.
func runListQuery(t *testing.T, args ...string) (url.Values, error) {
t.Helper()

var got url.Values
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -60,9 +70,8 @@ func captureListQuery(t *testing.T, args ...string) url.Values {
root.SetOut(io.Discard)
root.SetErr(io.Discard)

require.NoError(t, root.Execute())
require.NotNil(t, got, "the command made no list request")
return got
err := root.Execute()
return got, err
}

func TestEventListSendsNewFilters(t *testing.T) {
Expand Down Expand Up @@ -128,13 +137,16 @@ func TestListFiltersAreOmittedWhenUnset(t *testing.T) {
// forgot, which is how they would get added — this pins the omission as
// intentional. The MCP side is pinned by
// TestPluralToolsOmitParametersHiddenFromTheAPIDocs.
//
// include is not in this list: the API documents it on these endpoints, and
// the CLI offers it as --include; see TestListIncludeData.
func TestListCommandsOmitFlagsHiddenFromTheAPIDocs(t *testing.T) {
root := &cobra.Command{Use: "hookdeck"}
root.AddCommand(newGatewayCmd().cmd)

cases := map[string][]string{
"event": {"bulk-retry-id", "include", "progressive", "event-data-id", "cli-user-id"},
"request": {"bulk-retry-id", "include", "progressive"},
"event": {"bulk-retry-id", "progressive", "event-data-id", "cli-user-id"},
"request": {"bulk-retry-id", "progressive"},
}

for resource, hidden := range cases {
Expand All @@ -151,3 +163,37 @@ func TestListCommandsOmitFlagsHiddenFromTheAPIDocs(t *testing.T) {
})
}
}

// TestListIncludeData covers --include on both list commands, which share
// applyIncludeData but each call it separately, so each is tested.
func TestListIncludeData(t *testing.T) {
for _, resource := range []string{"event", "request"} {
t.Run(resource+" sends include and the smaller default limit", func(t *testing.T) {
query := captureListQuery(t, "gateway", resource, "list", "--include", "data", "--output", "json")
assert.Equal(t, "data", query.Get("include"))
assert.Equal(t, "10", query.Get("limit"),
"a page of payloads must not default to 100")
})

t.Run(resource+" honours an explicit limit", func(t *testing.T) {
query := captureListQuery(t, "gateway", resource, "list", "--include", "data", "--output", "json", "--limit", "50")
assert.Equal(t, "data", query.Get("include"))
assert.Equal(t, "50", query.Get("limit"))
})

t.Run(resource+" without include is unchanged", func(t *testing.T) {
query := captureListQuery(t, "gateway", resource, "list")
assert.NotContains(t, query, "include")
assert.Equal(t, "100", query.Get("limit"))
})

// The list output prints IDs and a summary line, so payloads fetched
// for it would be thrown away. Refused before any request is made.
t.Run(resource+" refuses include without --output json", func(t *testing.T) {
query, err := runListQuery(t, "gateway", resource, "list", "--include", "data")
require.Error(t, err)
assert.Contains(t, err.Error(), "--output json")
assert.Nil(t, query, "no list request should be made")
})
}
}
44 changes: 44 additions & 0 deletions pkg/cmd/list_include.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cmd

import (
"errors"
"strconv"

"github.com/spf13/cobra"
)

// includeDataDefaultLimit is the page size sent when --include is set without
// --limit, matching the MCP list tools.
//
// The usual default of 100 is fine for IDs and metadata. With payloads it is a
// hundred webhook bodies in one response, which is more than a terminal or a
// script piping to jq wants by accident. An explicit --limit is honoured.
const includeDataDefaultLimit = 10

// includeFlagUsage is the --include help for event list and request list.
func includeFlagUsage(resource string) string {
return `Set to "data" to include each ` + resource + `'s payload (headers, body, path, parsed query) in the results. ` +
`Requires --output json. Responses grow with every payload: when --limit is not set it defaults to ` +
strconv.Itoa(includeDataDefaultLimit) + ` instead of 100`
}

// applyIncludeData adds include to the list query and returns the limit to
// send.
//
// The API declares include on GET /events and GET /requests with one value,
// "data"; any other value is left for the API to reject. --include is refused
// without --output json because the list output prints IDs and a summary
// line, so the payloads would be fetched and then not shown.
func applyIncludeData(cmd *cobra.Command, include, output string, limit int, params map[string]string) (int, error) {
if include == "" {
return limit, nil
}
if output != "json" {
return 0, errors.New("--include requires --output json: the list output does not show payloads")
}
params["include"] = include
if !cmd.Flags().Changed("limit") {
return includeDataDefaultLimit, nil
}
return limit, nil
}
27 changes: 25 additions & 2 deletions pkg/cmd/request_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type requestEventsCmd struct {
parsedQuery string
orderBy string
dir string
include string
}

func newRequestEventsCmd() *requestEventsCmd {
Expand All @@ -59,10 +60,17 @@ func newRequestEventsCmd() *requestEventsCmd {
Filters match ` + "`hookdeck gateway event list`" + `: this command queries the same event
collection, narrowed to one request.

Use --include=data with --output=json to get each event's payload in the results. Without
--limit, the limit is then 10 instead of 100.

When the request has more events than fit on one page, a note on stderr gives the cursor to
pass to --next for the following page.

Examples:
hookdeck gateway request events req_abc123
hookdeck gateway request events req_abc123 --status FAILED
hookdeck gateway request events req_abc123 --destination-id des_abc123`,
hookdeck gateway request events req_abc123 --destination-id des_abc123
hookdeck gateway request events req_abc123 --include data --output json`,
RunE: rc.runRequestEventsCmd,
}

Expand Down Expand Up @@ -97,6 +105,7 @@ Examples:
rc.cmd.Flags().StringVar(&rc.body, "body", "", "Filter by body (JSON string)")
rc.cmd.Flags().StringVar(&rc.path, "path", "", "Filter by path")
rc.cmd.Flags().StringVar(&rc.parsedQuery, "parsed-query", "", "Filter by parsed query (JSON string)")
rc.cmd.Flags().StringVar(&rc.include, "include", "", includeFlagUsage("event"))
rc.cmd.Flags().StringVar(&rc.orderBy, "order-by", "", "Sort key (e.g. created_at)")
rc.cmd.Flags().StringVar(&rc.dir, "dir", "", "Sort direction (asc, desc)")
rc.cmd.Flags().IntVar(&rc.limit, "limit", 100, "Limit number of results")
Expand All @@ -122,7 +131,12 @@ func (rc *requestEventsCmd) runRequestEventsCmd(cmd *cobra.Command, args []strin
requestID := args[0]
client := Config.GetAPIClient()
ctx := context.Background()
params := map[string]string{"limit": strconv.Itoa(rc.limit)}
params := map[string]string{}
limit, err := applyIncludeData(cmd, rc.include, rc.output, rc.limit, params)
if err != nil {
return err
}
params["limit"] = strconv.Itoa(limit)
if rc.next != "" {
params["next"] = rc.next
}
Expand Down Expand Up @@ -211,6 +225,15 @@ func (rc *requestEventsCmd) runRequestEventsCmd(cmd *cobra.Command, args []strin
return fmt.Errorf("failed to list request events: %w", err)
}

// The output carries no pagination cursor (the JSON is a bare array), so a
// page the API cut short would look complete. When the API says there is
// more, say so on stderr, which leaves the JSON on stdout intact, and give
// the cursor --next takes. This applies with or without --include: the
// --include default of 10 makes it likelier, but 100 events is also a page.
if next := resp.Pagination.Next; next != nil && *next != "" {
fmt.Fprintf(os.Stderr, "More events exist for this request. Pass --next %s for the next page, or a larger --limit.\n", *next)
}

if rc.output == "json" {
if len(resp.Models) == 0 {
fmt.Println("[]")
Expand Down
Loading
Loading