Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fe9134c
Pin the SDK to the main commit carrying the event-feed operations
jeremy Sep 16, 2026
4fb6a92
Add events poll, inbox, and stream-ticket commands on the account eve…
jeremy Sep 16, 2026
0237fb6
Name the lane's own identifier in an invalid --since, and re-hash the…
jeremy Sep 16, 2026
571cb3b
Preserve filters when re-entering a gone position, and count feed rows
jeremy Sep 16, 2026
d61a138
Run the event-feed smoke suite
jeremy Sep 16, 2026
07b0639
Carry the poll's own filters into every re-entry it advertises
jeremy Sep 16, 2026
cc2df29
Adopt the SDK's per-lane 410 and the 400's reason
jeremy Sep 16, 2026
055f87a
Keep the stream-ticket mint off the MCP surface
jeremy Sep 16, 2026
ffab993
Hand the position back to the person, not just the machine
jeremy Sep 16, 2026
2becc61
Quote the position in the command it hands back
jeremy Sep 16, 2026
62c66dc
Re-hash the flake for the SDK re-pin
jeremy Sep 16, 2026
b283ecc
Say the walk was capped in --ids-only and --count too
jeremy Sep 16, 2026
66d8c26
Name the set the stream-ticket mint is actually excluded by
jeremy Sep 16, 2026
6e241bf
Withhold a cable URL that still carries the ticket anywhere
jeremy Sep 16, 2026
348a753
Resume from the filters the last page was minted under
jeremy Sep 16, 2026
250e295
Suppress --project when the id-or-url positional is optional too
jeremy Sep 16, 2026
70a68c4
Say 195 endpoints and name the event feed
jeremy Sep 16, 2026
ae1a934
Build the redacted cable URL's query instead of editing it
jeremy Sep 16, 2026
13f535a
Project the feed smoke reads down to what they assert
jeremy Sep 16, 2026
ca61413
Say that the redacted cable URL is a diagnostic
jeremy Sep 16, 2026
45c8cd0
Name v0.18.0 as the release the pin builds on
jeremy Sep 16, 2026
03edb00
Capture the redaction verdict, not the ticket it checks
jeremy Sep 16, 2026
ce9c288
Vet the redacted URL as rendered, not component by component
jeremy Sep 16, 2026
6687811
Refuse an entry flag given an empty value
jeremy Sep 16, 2026
50bc7cb
Write down why events keeps RunE alongside its subcommands
jeremy Sep 16, 2026
a56a169
State the redaction assertion's coverage component by component
jeremy Sep 16, 2026
3b6f8f3
Treat every ticket the response presents as the secret
jeremy Sep 16, 2026
aa3e458
Check the redacted URL's decoding, not its components
jeremy Sep 16, 2026
dbbff8e
Refuse a list filter given an empty value
jeremy Sep 16, 2026
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
89 changes: 88 additions & 1 deletion .surface
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ ARG basecamp documents uploads create 00 <file>
ARG basecamp documents vault create 00 <name>
ARG basecamp documents vaults create 00 <name>
ARG basecamp documents versions 00 <upload_id|url>
ARG basecamp events 00 <id|url>
ARG basecamp events 00 [id|url]
ARG basecamp file archive 00 <id|url>
ARG basecamp file doc create 00 <title>
ARG basecamp file doc create 01 [content]
Expand Down Expand Up @@ -746,6 +746,8 @@ CMD basecamp documents versions
CMD basecamp drafts
CMD basecamp drafts list
CMD basecamp events
CMD basecamp events poll
CMD basecamp events ticket
CMD basecamp file
CMD basecamp file archive
CMD basecamp file doc
Expand Down Expand Up @@ -880,6 +882,7 @@ CMD basecamp hillcharts
CMD basecamp hillcharts show
CMD basecamp hillcharts track
CMD basecamp hillcharts untrack
CMD basecamp inbox
CMD basecamp lineup
CMD basecamp lineup create
CMD basecamp lineup delete
Expand Down Expand Up @@ -7317,6 +7320,59 @@ FLAG basecamp events --stats type=bool
FLAG basecamp events --styled type=bool
FLAG basecamp events --todolist type=string
FLAG basecamp events --verbose type=count
FLAG basecamp events poll --account type=string
FLAG basecamp events poll --actor-types type=stringSlice
FLAG basecamp events poll --agent type=bool
FLAG basecamp events poll --all type=bool
FLAG basecamp events poll --buckets type=stringSlice
FLAG basecamp events poll --cache-dir type=string
FLAG basecamp events poll --count type=bool
FLAG basecamp events poll --creators type=stringSlice
FLAG basecamp events poll --exclude-performers type=stringSlice
FLAG basecamp events poll --help type=bool
FLAG basecamp events poll --hints type=bool
FLAG basecamp events poll --ids-only type=bool
FLAG basecamp events poll --in type=string
FLAG basecamp events poll --jq type=string
FLAG basecamp events poll --json type=bool
FLAG basecamp events poll --markdown type=bool
FLAG basecamp events poll --max-pages type=int
FLAG basecamp events poll --md type=bool
FLAG basecamp events poll --no-hints type=bool
FLAG basecamp events poll --no-stats type=bool
FLAG basecamp events poll --performers type=stringSlice
FLAG basecamp events poll --position type=string
FLAG basecamp events poll --profile type=string
FLAG basecamp events poll --project type=string
FLAG basecamp events poll --quiet type=bool
FLAG basecamp events poll --since type=string
FLAG basecamp events poll --stats type=bool
FLAG basecamp events poll --styled type=bool
FLAG basecamp events poll --todolist type=string
FLAG basecamp events poll --types type=stringSlice
FLAG basecamp events poll --verbose type=count
FLAG basecamp events ticket --account type=string
FLAG basecamp events ticket --agent type=bool
FLAG basecamp events ticket --cache-dir type=string
FLAG basecamp events ticket --count type=bool
FLAG basecamp events ticket --help type=bool
FLAG basecamp events ticket --hints type=bool
FLAG basecamp events ticket --ids-only type=bool
FLAG basecamp events ticket --in type=string
FLAG basecamp events ticket --jq type=string
FLAG basecamp events ticket --json type=bool
FLAG basecamp events ticket --markdown type=bool
FLAG basecamp events ticket --md type=bool
FLAG basecamp events ticket --no-hints type=bool
FLAG basecamp events ticket --no-stats type=bool
FLAG basecamp events ticket --profile type=string
FLAG basecamp events ticket --project type=string
FLAG basecamp events ticket --quiet type=bool
FLAG basecamp events ticket --show-secret type=bool
FLAG basecamp events ticket --stats type=bool
FLAG basecamp events ticket --styled type=bool
FLAG basecamp events ticket --todolist type=string
FLAG basecamp events ticket --verbose type=count
FLAG basecamp file --account type=string
FLAG basecamp file --agent type=bool
FLAG basecamp file --cache-dir type=string
Expand Down Expand Up @@ -10669,6 +10725,34 @@ FLAG basecamp hillcharts untrack --styled type=bool
FLAG basecamp hillcharts untrack --todolist type=string
FLAG basecamp hillcharts untrack --todoset type=string
FLAG basecamp hillcharts untrack --verbose type=count
FLAG basecamp inbox --account type=string
FLAG basecamp inbox --agent type=bool
FLAG basecamp inbox --all type=bool
FLAG basecamp inbox --buckets type=stringSlice
FLAG basecamp inbox --cache-dir type=string
FLAG basecamp inbox --count type=bool
FLAG basecamp inbox --help type=bool
FLAG basecamp inbox --hints type=bool
FLAG basecamp inbox --ids-only type=bool
FLAG basecamp inbox --in type=string
FLAG basecamp inbox --jq type=string
FLAG basecamp inbox --json type=bool
FLAG basecamp inbox --markdown type=bool
FLAG basecamp inbox --max-pages type=int
FLAG basecamp inbox --md type=bool
FLAG basecamp inbox --no-hints type=bool
FLAG basecamp inbox --no-stats type=bool
FLAG basecamp inbox --position type=string
FLAG basecamp inbox --profile type=string
FLAG basecamp inbox --project type=string
FLAG basecamp inbox --quiet type=bool
FLAG basecamp inbox --reasons type=stringSlice
FLAG basecamp inbox --since type=string
FLAG basecamp inbox --stats type=bool
FLAG basecamp inbox --styled type=bool
FLAG basecamp inbox --todolist type=string
FLAG basecamp inbox --types type=stringSlice
FLAG basecamp inbox --verbose type=count
FLAG basecamp lineup --account type=string
FLAG basecamp lineup --agent type=bool
FLAG basecamp lineup --cache-dir type=string
Expand Down Expand Up @@ -18462,6 +18546,8 @@ SUB basecamp documents versions
SUB basecamp drafts
SUB basecamp drafts list
SUB basecamp events
SUB basecamp events poll
SUB basecamp events ticket
SUB basecamp file
SUB basecamp file archive
SUB basecamp file doc
Expand Down Expand Up @@ -18596,6 +18682,7 @@ SUB basecamp hillcharts
SUB basecamp hillcharts show
SUB basecamp hillcharts track
SUB basecamp hillcharts untrack
SUB basecamp inbox
SUB basecamp lineup
SUB basecamp lineup create
SUB basecamp lineup delete
Expand Down
1 change: 1 addition & 0 deletions .surface-breaking
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ARG basecamp comment 00 <id|url>
ARG basecamp comment 01 <content>
ARG basecamp comments list 00 <id|url>
ARG basecamp done 00 <id|url>...
ARG basecamp events 00 <id|url>
ARG basecamp message 00 <title>
ARG basecamp message 01 [body]
ARG basecamp react 00 <content>
Expand Down
15 changes: 10 additions & 5 deletions API-COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github.

| Status | Sections | Endpoints |
|--------|----------|-----------|
| ✅ Implemented | 50 | 192 |
| ✅ Implemented | 51 | 195 |
| ⚠️ Blocked | 0 | 0 |
| ⏭️ Out of scope | 4 | 12 |
| **Total tracked** | **54** | **204** |
| **Total tracked** | **55** | **207** |

**192 of 192 tracked in-scope endpoints.** The client-admission endpoints
**195 of 195 tracked in-scope endpoints.** The client-admission endpoints
basecamp/bc3#13098 added — `PUT /projects/:id/people/client_users.json` and
`POST`/`DELETE /projects/:id/client_enablement.json` — land as `people clients`.
SDK v0.16.0 adds the three to-do
Expand Down Expand Up @@ -48,8 +48,11 @@ Out-of-scope sections are excluded from parity totals and scripts: chatbots (dif

> Note: the per-row `Endpoints` column in the Coverage by Section table sums higher than the Summary totals above. The discrepancy predates the BC5 baseline; the row count (48 sections) is authoritative for the `Since` column. Reconciling endpoint counts is pre-existing maintenance, tracked separately.

**SDK version:** v0.16.0 (adds the to-do list template library and asynchronous
copy operations; `internal/version/sdk-provenance.json` is authoritative). The
**SDK version:** the pin in `go.mod`, with
`internal/version/sdk-provenance.json` authoritative for the exact commit. The
event-feed operations are unreleased, so that pin is a basecamp-sdk `main`
pseudo-version rather than a release tag; it builds on v0.18.0, the latest
release. The
command surface below largely dates to the v0.12.0 bump, which added 20 exported
Go methods over 13 new backend operations; the extra seven wrapped endpoints
that already existed but were reachable only through the raw generated client,
Expand Down Expand Up @@ -229,6 +232,8 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
| schedules | 2 | `schedule` | ✅ | BC4 | - | Schedule container + settings |
| schedule_entries | 5 | `schedule` | ✅ | BC4 | - | list, show, create, update, occurrences. Create supports `--subscribe`/`--no-subscribe` |
| events | 1 | `events` | ✅ | BC4 | - | Recording change audit trail |
| **Event Feed** |
| event_feed | 3 | `events poll`, `events ticket`, `inbox` | ✅ | BC5 | - | The account-wide event feed: `GET /events.json` (`events poll`), `GET /inbox.json` (`inbox`, agent principals only), `POST /events/stream_ticket.json` (`events ticket`). Pagination is the body envelope — a durable `position` and a `next` continuation URL — not the Link-header page walk, so `--all` walks `next` rather than page numbers. The live WebSocket lane the ticket opens is out of scope for the CLI: the SDK's cable client carries a WebSocket dependency the CLI does not take. The mint is a CLI command only — it is excluded from the MCP surface, whose dispatcher would return the bearer verbatim into a model transcript and which could not open the socket anyway |
| **Webhooks** |
| webhooks | 7 | `webhooks` | ✅ | BC4 | - | list, show, create, update, delete |
| **Templates** |
Expand Down
12 changes: 10 additions & 2 deletions STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,16 @@ existing group noun is not allowed.

Shortcut commands without a sibling plural group — `search`, `url`,
`recordings`, `timesheet`, `assignments`, `notifications`, `setup`, `completion`
— may have both `RunE` and subcommands. `scripts/check-bare-groups.sh` enforces
this with an allowlist.
— may have both `RunE` and subcommands.

`events` is the one plural noun in that set. `events <id|url>` reads a single
recording's change history and predates the account-wide event feed that
`events poll` and `events ticket` now serve, so the bare form stays an action
rather than becoming a group: turning it into one would break every existing
`basecamp events <id>` invocation to gain nothing.

`scripts/check-bare-groups.sh` enforces this with an allowlist; a command added
there belongs in this section too, with the reason it is an exception.

## File Organization

Expand Down
62 changes: 62 additions & 0 deletions e2e/event_feed.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bats
# event_feed.bats - Account event feed: entry-point and filter validation
#
# Every case here is refused before a request leaves the process, so the suite
# needs no network and no live feed.

load test_helper

setup_extra() {
create_credentials
create_global_config '{"account_id": 99999}'
}

@test "events poll rejects --since and --position together" {
run basecamp events poll --since now --position abc
assert_failure
assert_output_contains "mutually exclusive"
}

@test "events poll rejects a --since that is not an entry point" {
run basecamp events poll --since 2026-07-14
assert_failure
assert_output_contains "Invalid --since"
}

@test "events poll rejects a non-numeric bucket filter" {
run basecamp events poll --buckets one
assert_failure
assert_output_contains "Invalid --buckets id"
}

@test "events poll rejects an actor type outside agent and person" {
run basecamp events poll --actor-types robot
assert_failure
assert_output_contains "Invalid --actor-types"
}

@test "events poll --agent --help documents position handling" {
run basecamp events poll --agent --help
assert_success
assert_output_contains "position"
assert_output_contains "Deduplicate by event id"
}

@test "events ticket --agent --help warns against logging the ticket" {
run basecamp events ticket --agent --help
assert_success
assert_output_contains "show-secret"
}

@test "inbox rejects --since and --position together" {
run basecamp inbox --since now --position abc
assert_failure
assert_output_contains "mutually exclusive"
}

@test "inbox --agent --help documents addressing_id dedupe" {
run basecamp inbox --agent --help
assert_success
assert_output_contains "addressing_id"
assert_output_contains "agent"
}
14 changes: 12 additions & 2 deletions e2e/events.bats
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ load test_helper

# Missing context errors

@test "events without recording id shows error" {
@test "events without recording id shows the group's help" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp events
assert_success
assert_output_contains "poll"
assert_output_contains "ticket"
}

@test "events with a non-numeric recording id shows error" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp events not-an-id
assert_failure
assert_output_contains "ID required"
assert_output_contains "Invalid ID"
}

# Help
Expand Down
4 changes: 4 additions & 0 deletions e2e/smoke/.qa-allowlist
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ test_cards_move_to_wormhole_needs_a_linked_wormhole_and_is_destructive # needs l

# Bubble Ups — BC5-added endpoint, may not exist on all QA environments
test_notifications_bubbleups_lists_bubble_ups # endpoint not available on some servers — smoke

# Inbox — served to agent principals only; a person's smoke token gets 403,
# which the test asserts before marking the gap.
test_inbox_polls_addressed_items # agents-only lane, smoke token is a person — smoke
1 change: 1 addition & 0 deletions e2e/smoke/run_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ level0=(
"$SMOKE_DIR"/smoke_checkins.bats
"$SMOKE_DIR"/smoke_schedule.bats
"$SMOKE_DIR"/smoke_config_local.bats
"$SMOKE_DIR"/smoke_event_feed.bats
)
level0_exist=()
for f in "${level0[@]}"; do
Expand Down
56 changes: 56 additions & 0 deletions e2e/smoke/smoke_event_feed.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bats
# smoke_event_feed.bats - Level 0: Account event feed operations
#
# Nothing here captures a position, a ticket or a cable URL: the first is a
# signed resume token and the other two are bearers, and bats keeps $output —
# a failing assertion prints it in full (test_helper.bash:118) into logs
# somebody keeps. So every read projects the envelope down to the fields it
# asserts with --jq, rather than capturing the whole thing and trusting
# redaction. Entering at the present (--since now) also keeps these reads
# bounded — a since=0 replay would walk the account's served history.
#
# This sits in Level 0 alongside the other read-only suites even though the
# ticket mint is a POST: it writes no record, mints a stateless signed token
# that expires in about two minutes, and leaves nothing behind for a parallel
# suite to trip over.

load smoke_helper
Comment thread
jeremy marked this conversation as resolved.

setup_file() {
ensure_token || return 1
}

@test "events poll enters the feed at the present" {
run_smoke basecamp events poll --since now --jq '{ok: .ok}'
assert_success
assert_json_value '.ok' 'true'
}

@test "events poll rejects a since that is not an entry point" {
run_smoke basecamp events poll --since yesterday --json
assert_failure
assert_output_contains "Invalid --since"
}

# The comparison happens inside --jq so that only its verdict is captured.
# Projecting the ticket itself and comparing here would mean that the one
# regression this test exists to catch — redaction failing — is also the one
# that puts a live bearer in the failure output.
@test "events ticket mints a redacted ticket" {
run_smoke basecamp events ticket \
--jq '{ok: .ok, error: .error, ticket_redacted: (.data.ticket == "[REDACTED]"), url_redacted: ((.data.url // "") | contains("REDACTED"))}'
assert_success
assert_json_value '.ok' 'true'
assert_json_value '.ticket_redacted' 'true'
assert_json_value '.url_redacted' 'true'
}

@test "inbox polls addressed items" {
run_smoke basecamp inbox --since now --jq '{ok: .ok, error: .error}'
# The inbox is served to agent principals only; a person's token gets 403.
if [[ "$status" -ne 0 ]]; then
assert_output_contains "agent principals only"
mark_unverifiable "Inbox is agents-only and this token is not an agent's"
fi
assert_json_value '.ok' 'true'
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
charm.land/bubbles/v2 v2.2.1
charm.land/bubbletea/v2 v2.0.9
charm.land/lipgloss/v2 v2.0.6
github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916083520-5acb2f9aaca9
github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916130353-a8046263835c
github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d
github.com/basecamp/surfguard/go v0.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916083520-5acb2f9aaca9 h1:W0R9T9iy40bY+RvGIozG7Sz9e456yl3QDGOFDniCaH4=
github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916083520-5acb2f9aaca9/go.mod h1:kIBDYwPMMD59PadNGxpH0YTQuI+blFPZ8MelGI0RK5Q=
github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916130353-a8046263835c h1:FJQx85IjtQkA2rLAHrO77op3VRA9b5U4S6qtdjSNdIU=
github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916130353-a8046263835c/go.mod h1:kIBDYwPMMD59PadNGxpH0YTQuI+blFPZ8MelGI0RK5Q=
github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d h1:jAzDrCCzDpIwhbFT1xVVs0z2xpXoDEkomHfKB2bUUp8=
github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d/go.mod h1:iTBTaWvsPEFIcZfkxQHEfISyJ6sZ7036K6bNx0RY3EE=
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4=
Expand Down
Loading
Loading