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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 396 additions & 0 deletions .surface

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions API-COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github.

| Status | Sections | Endpoints |
|--------|----------|-----------|
| ✅ Implemented | 52 | 204 |
| ✅ Implemented | 53 | 207 |
| ⚠️ Blocked | 0 | 0 |
| ⏭️ Out of scope | 4 | 12 |
| **Total tracked** | **56** | **216** |
| **Total tracked** | **57** | **219** |

**204 of 204 tracked in-scope endpoints.** Moving a document, upload or folder
**207 of 207 tracked in-scope endpoints.** The three Pings endpoints bc3#13943
documented — `GET /circles.json`, `GET /circles/:id.json` and `POST
/circles.json` — land as `pings`. Moving a document, upload or folder
between folders (`POST /recordings/:id/filing.json`, documented by bc3#13644 and
modelled by basecamp/basecamp-sdk#960) lands as `files move`. The eight subtask endpoints bc3
#12659 documented — the flat `/recordings/:id/subtasks.json` and
Expand Down Expand Up @@ -209,6 +211,7 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
| message_boards | 3 | `messageboards` | ✅ | BC4 | - | Container, accessed via project dock |
| message_types | 5 | `messagetypes` | ✅ | BC4 | - | list, show, create, update, delete. Bucket-scoped (`/buckets/{id}/categories…`); commands are project-scoped via `--in`/`--project` |
| campfires | 14 | `chat` | ✅ | BC4 | - | list, messages, post, line show/update/delete. @mentions in content |
| pings | 3 | `pings` | ✅ | BC5 | - | list (`GET /circles.json`: active pings with at least one line, newest first, unpaginated), show (`GET /circles/:id.json`, archived included), start (`POST /circles.json`, which opens or reuses: an active direct ping is always reused, a group with the same people unless `--new-group`). A ping is a Circle bucket with one Campfire, so `messages`, `post` and `upload` run through the Campfire lines endpoints on the ping's `campfire_id`, with @mentions resolved among the ping's people. `<ping>` is an id, a ping URL, or a person (a direct ping: read from the list, or started when writing). `show` and `chat line` take ping URLs (`/circles/:id`, `/circles/:id@:line`). 403 `Pings have been disabled` and 422 (an agent starting a ping with nobody who can ping) surface as errors. Requires basecamp/bc3#13943 |
| comments | 8 | `comment`, `comments` | ✅ | BC4 | - | list, show, thread, create, update. @mentions in content. `show` surfaces `reply_target` + paste-ready `mention` from its single Get (no new calls). `thread` composes Get + parent recording (via type endpoint) + List into a deterministic reply-ready context (no new endpoints) |
| boosts | 6 | `boost`, `react` | ✅ | BC4 | - | list (recording + event), show, create (recording + event), delete. No account-wide listing — BC5 withdrew `/boosts.json` (basecamp/bc3#12464); temporary, returns via basecamp/bc3#12463 |
| notifications | 2 | `notifications` | ✅ | BC4 | - | list, mark as read (BC5: `bubble_ups`/`scheduled_bubble_ups` sections; `memories` is BC4-only) |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ basecamp search "authentication" # Search across projects
basecamp files list --in 12345 # List docs & files
basecamp cards list --in 12345 # List cards (Kanban)
basecamp chat post "Hello" --in 12345 # Post to chat
basecamp pings post Jane "Lunch?" # Ping someone directly
basecamp comments create 67890 "@Jane.Smith, done!" # Comment with @mention
```

Expand Down
71 changes: 71 additions & 0 deletions e2e/pings.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bats
# pings.bats - Test pings command input handling

load test_helper


# Help

@test "pings without subcommand shows help" {
run basecamp pings
assert_success
assert_output_contains "COMMANDS"
assert_output_contains "start"
}

@test "ping is an alias of pings" {
run basecamp ping
assert_success
assert_output_contains "COMMANDS"
}

@test "pings list help says when a ping appears" {
run basecamp pings list --help
assert_success
assert_output_contains "once someone has posted"
}

@test "chat list help points to pings" {
run basecamp chat list --help
assert_success
assert_output_contains "basecamp pings list"
}


# Input errors that need no network

@test "pings start --new-group needs two people" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp pings start Jane --new-group
assert_failure
assert_output_contains "--new-group needs two or more people"
}

@test "pings show rejects a URL that is not a ping" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp pings show https://3.basecamp.com/99999/buckets/1/todos/2
assert_failure
assert_output_contains "expected a ping URL"
}

@test "pings show rejects a URL on an untrusted host" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp pings show https://evil.example.com/99999/circles/1
assert_failure
assert_output_contains "untrusted host"
}

@test "pings messages --limit without value shows error" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp pings messages 1 --limit
assert_failure
assert_output_contains "--limit requires a value"
}
8 changes: 8 additions & 0 deletions e2e/smoke/smoke_aliases.bats
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ load smoke_helper
mark_out_of_scope "Alias for messages — tested via canonical form"
}

@test "ping is out of scope" {
mark_out_of_scope "Alias for pings — tested via canonical form"
}

@test "pings with is out of scope" {
mark_out_of_scope "Alias for pings start — tested via canonical form"
}

@test "project is out of scope" {
mark_out_of_scope "Alias for projects — tested via canonical form"
}
Expand Down
51 changes: 51 additions & 0 deletions e2e/smoke/smoke_pings.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bats
# smoke_pings.bats - Level 0: Pings (private conversations)

load smoke_helper

setup_file() {
ensure_token || return 1
}

@test "pings list returns pings" {
run_smoke basecamp pings list --json
assert_success
assert_json_value '.ok' 'true'

echo "$output" | jq -r '.data[0].id // empty' > "$BATS_FILE_TMPDIR/ping_id"
}

@test "pings show returns a ping" {
local id_file="$BATS_FILE_TMPDIR/ping_id"
[[ -s "$id_file" ]] || mark_unverifiable "No pings to show"
local ping_id
ping_id=$(<"$id_file")

run_smoke basecamp pings show "$ping_id" --json
assert_success
assert_json_value '.ok' 'true'
assert_json_not_null '.data.campfire_id'
}

@test "pings messages returns lines" {
local id_file="$BATS_FILE_TMPDIR/ping_id"
[[ -s "$id_file" ]] || mark_unverifiable "No pings to read"
local ping_id
ping_id=$(<"$id_file")

run_smoke basecamp pings messages "$ping_id" --limit 1 --json
assert_success
assert_json_value '.ok' 'true'
}

@test "pings start is out of scope" {
mark_out_of_scope "Mutating - opens or creates a ping with a real person; covered by the live round-trip"
}

@test "pings post is out of scope" {
mark_out_of_scope "Mutating - posts into a private conversation with a real person; covered by the live round-trip"
}

@test "pings upload is out of scope" {
mark_out_of_scope "Mutating - uploads into a private conversation with a real person; covered by the live round-trip"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
charm.land/bubbles/v2 v2.2.1
charm.land/bubbletea/v2 v2.1.0
charm.land/lipgloss/v2 v2.0.6
github.com/basecamp/basecamp-sdk/go v0.24.0
github.com/basecamp/basecamp-sdk/go v0.26.0
Comment thread
robzolkos marked this conversation as resolved.
Comment thread
robzolkos marked this conversation as resolved.
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 @@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/basecamp/actioncable-go v1.2.0 h1:t0MxlfMTcmaGXZ5ztgKRs7ZiY1MwXtBRwSSSAh1ofVg=
github.com/basecamp/actioncable-go v1.2.0/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs=
github.com/basecamp/basecamp-sdk/go v0.24.0 h1:kJGxd96qgtp5IH9eAVkzoo8LiHs9Raahcb5dWUTTSEI=
github.com/basecamp/basecamp-sdk/go v0.24.0/go.mod h1:vdplxIcZMYSAIOvVm7AOIgLBUAPoJHESStTh9mF/ww4=
github.com/basecamp/basecamp-sdk/go v0.26.0 h1:ENIK+0Q9WWRCzcJjz0RLLpvAMSfKoQOl8lHRumaEeOg=
github.com/basecamp/basecamp-sdk/go v0.26.0/go.mod h1:PAp3gBnEPH7LtAyWXwgQ1tfmCAJiMkRHgRJAKT/Xefc=
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
2 changes: 1 addition & 1 deletion internal/cli/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ var curatedCategories = []struct {
{
heading: "CORE COMMANDS",
category: "Core Commands",
include: map[string]bool{"projects": true, "todos": true, "todolists": true, "messages": true, "chat": true, "cards": true, "files": true, "checkins": true, "schedule": true},
include: map[string]bool{"projects": true, "todos": true, "todolists": true, "messages": true, "chat": true, "pings": true, "cards": true, "files": true, "checkins": true, "schedule": true},
},
{
heading: "SHORTCUTS",
Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func Execute() {
cmd.AddCommand(commands.NewSearchCmd())
cmd.AddCommand(commands.NewRecordingsCmd())
cmd.AddCommand(commands.NewChatCmd())
cmd.AddCommand(commands.NewPingsCmd())
cmd.AddCommand(commands.NewScheduleCmd())
cmd.AddCommand(commands.NewFilesCmd())
cmd.AddCommand(commands.NewVaultsCmd())
Expand Down
Loading
Loading