Skip to content

Commit 8ddf7e4

Browse files
authored
Embed a URL that stands alone on its line in rich text (#927)
* Embed a URL that stands alone on its line in rich text An agent writing a card, comment, message, document, to-do or schedule entry had no way to embed a video or a Dash chart: a URL stayed a link, where a person pasting it in the editor gets an embed. Now, in Markdown content, a URL alone on its line becomes an embed when Basecamp can embed it. A titled link, a URL inside a sentence, list, quote, table or code, and anything sent with --format html are left as written. The CLI creates every embed first, then waits for them together, polling as the editor does, and inserts only the ones that are ready. A URL Basecamp can't embed stays a link quietly; a refusal, a rate limit, or a provider with nothing to embed or too slow to answer stays a link with a warning. Nothing fails the post. embeds create and embeds show cover the new SDK methods, and are how to embed with --format html. The MCP catalog serves them in the files domain. The skill, --format help, API coverage, surface snapshot and a smoke test say so. * Bound the embed step, poll embeds together, and leave HTML and references alone One ten-second budget now covers every embed request, not just the waiting, so a hung request can't hold the post. Pending embeds are polled together, so one slow provider no longer stops the others being seen. The first failure that isn't a refusal ends the embed step, leaving the rest as links, so a struggling server costs the post one request, not one per URL, and can't trip the circuit breaker the post goes through. A paragraph holding inline HTML is left as written, since splitting it would break the HTML, and so is a URL with a character reference, which would reach Basecamp undecoded. * End the embed step at a create failure too, and skip HTML nested in a paragraph A create failure stopped further creates but still polled the embeds created before it; now it ends the step. Inline HTML inside emphasis or a link also keeps its paragraph as written. * Read a standalone URL's scheme in any case * Bump the SDK to treat the embed limit's 429 as a verdict CreateEmbed no longer retries on 429 and its hooks name the operation CreateEmbed, so a headerless 429 from the hourly new-embed limit sets no CLI-wide block that would hold back the post it was for. * Update the Nix vendorHash for the SDK bump * Pin the SDK at its Embeds pull request's head * Pin the SDK at its Embeds pull request's new head * Sanitize embed warnings, and say the attachment tag is in --json URLs and server error text in the embed warnings go through SanitizeSingleLine, so a control character or a newline can't restyle the terminal or forge a line. The styled renderer turns any HTML into a placeholder, so the help says the attachment tag comes in --json's attachment field. * Pin the SDK at its Embeds pull request's converged head * Stop asking for embeds quietly where Basecamp doesn't serve them A 404 from create means the endpoint isn't there, as before the server change deploys, so asking again for each URL only spends the budget. * Run the embeds smoke test, and accept a URL-only body in its eval make smoke lists its files, and smoke_embeds.bats wasn't among them. The eval also takes the chart URL as the whole positional body, which the CLI embeds the same way. * Pin the SDK at its Embeds pull request's rebased head * Pin the SDK at its v0.27.0 release
1 parent ccfc3c8 commit 8ddf7e4

26 files changed

Lines changed: 1544 additions & 33 deletions

‎.surface‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ ARG basecamp documents uploads create 00 <file>
182182
ARG basecamp documents vault create 00 <name>
183183
ARG basecamp documents vaults create 00 <name>
184184
ARG basecamp documents versions 00 <upload_id|url>
185+
ARG basecamp embeds create 00 <url>
186+
ARG basecamp embeds show 00 <sgid>
185187
ARG basecamp events 00 [id|url]
186188
ARG basecamp file archive 00 <id|url>
187189
ARG basecamp file doc create 00 <title>
@@ -788,6 +790,9 @@ CMD basecamp documents vaults list
788790
CMD basecamp documents versions
789791
CMD basecamp drafts
790792
CMD basecamp drafts list
793+
CMD basecamp embeds
794+
CMD basecamp embeds create
795+
CMD basecamp embeds show
791796
CMD basecamp events
792797
CMD basecamp events poll
793798
CMD basecamp events ticket
@@ -7532,6 +7537,69 @@ FLAG basecamp drafts list --stats type=bool
75327537
FLAG basecamp drafts list --styled type=bool
75337538
FLAG basecamp drafts list --todolist type=string
75347539
FLAG basecamp drafts list --verbose type=count
7540+
FLAG basecamp embeds --account type=string
7541+
FLAG basecamp embeds --agent type=bool
7542+
FLAG basecamp embeds --cache-dir type=string
7543+
FLAG basecamp embeds --count type=bool
7544+
FLAG basecamp embeds --help type=bool
7545+
FLAG basecamp embeds --hints type=bool
7546+
FLAG basecamp embeds --ids-only type=bool
7547+
FLAG basecamp embeds --in type=string
7548+
FLAG basecamp embeds --jq type=string
7549+
FLAG basecamp embeds --json type=bool
7550+
FLAG basecamp embeds --markdown type=bool
7551+
FLAG basecamp embeds --md type=bool
7552+
FLAG basecamp embeds --no-hints type=bool
7553+
FLAG basecamp embeds --no-stats type=bool
7554+
FLAG basecamp embeds --profile type=string
7555+
FLAG basecamp embeds --project type=string
7556+
FLAG basecamp embeds --quiet type=bool
7557+
FLAG basecamp embeds --stats type=bool
7558+
FLAG basecamp embeds --styled type=bool
7559+
FLAG basecamp embeds --todolist type=string
7560+
FLAG basecamp embeds --verbose type=count
7561+
FLAG basecamp embeds create --account type=string
7562+
FLAG basecamp embeds create --agent type=bool
7563+
FLAG basecamp embeds create --cache-dir type=string
7564+
FLAG basecamp embeds create --count type=bool
7565+
FLAG basecamp embeds create --help type=bool
7566+
FLAG basecamp embeds create --hints type=bool
7567+
FLAG basecamp embeds create --ids-only type=bool
7568+
FLAG basecamp embeds create --in type=string
7569+
FLAG basecamp embeds create --jq type=string
7570+
FLAG basecamp embeds create --json type=bool
7571+
FLAG basecamp embeds create --markdown type=bool
7572+
FLAG basecamp embeds create --md type=bool
7573+
FLAG basecamp embeds create --no-hints type=bool
7574+
FLAG basecamp embeds create --no-stats type=bool
7575+
FLAG basecamp embeds create --profile type=string
7576+
FLAG basecamp embeds create --project type=string
7577+
FLAG basecamp embeds create --quiet type=bool
7578+
FLAG basecamp embeds create --stats type=bool
7579+
FLAG basecamp embeds create --styled type=bool
7580+
FLAG basecamp embeds create --todolist type=string
7581+
FLAG basecamp embeds create --verbose type=count
7582+
FLAG basecamp embeds show --account type=string
7583+
FLAG basecamp embeds show --agent type=bool
7584+
FLAG basecamp embeds show --cache-dir type=string
7585+
FLAG basecamp embeds show --count type=bool
7586+
FLAG basecamp embeds show --help type=bool
7587+
FLAG basecamp embeds show --hints type=bool
7588+
FLAG basecamp embeds show --ids-only type=bool
7589+
FLAG basecamp embeds show --in type=string
7590+
FLAG basecamp embeds show --jq type=string
7591+
FLAG basecamp embeds show --json type=bool
7592+
FLAG basecamp embeds show --markdown type=bool
7593+
FLAG basecamp embeds show --md type=bool
7594+
FLAG basecamp embeds show --no-hints type=bool
7595+
FLAG basecamp embeds show --no-stats type=bool
7596+
FLAG basecamp embeds show --profile type=string
7597+
FLAG basecamp embeds show --project type=string
7598+
FLAG basecamp embeds show --quiet type=bool
7599+
FLAG basecamp embeds show --stats type=bool
7600+
FLAG basecamp embeds show --styled type=bool
7601+
FLAG basecamp embeds show --todolist type=string
7602+
FLAG basecamp embeds show --verbose type=count
75357603
FLAG basecamp events --account type=string
75367604
FLAG basecamp events --agent type=bool
75377605
FLAG basecamp events --all type=bool
@@ -19520,6 +19588,9 @@ SUB basecamp documents vaults list
1952019588
SUB basecamp documents versions
1952119589
SUB basecamp drafts
1952219590
SUB basecamp drafts list
19591+
SUB basecamp embeds
19592+
SUB basecamp embeds create
19593+
SUB basecamp embeds show
1952319594
SUB basecamp events
1952419595
SUB basecamp events poll
1952519596
SUB basecamp events ticket

‎API-COVERAGE.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github.
66

77
| Status | Sections | Endpoints |
88
|--------|----------|-----------|
9-
| ✅ Implemented | 53 | 207 |
9+
| ✅ Implemented | 54 | 209 |
1010
| ⚠️ Blocked | 0 | 0 |
1111
| ⏭️ Out of scope | 4 | 12 |
12-
| **Total tracked** | **57** | **219** |
12+
| **Total tracked** | **58** | **221** |
1313

14-
**207 of 207 tracked in-scope endpoints.** The three Pings endpoints bc3#13943
14+
**209 of 209 tracked in-scope endpoints.** The two embed endpoints bc3 documents
15+
in `embeds.md` — `POST /embeds.json` and `GET /embeds/:sgid.json` — land as
16+
`embeds`. The three Pings endpoints bc3#13943
1517
documented — `GET /circles.json`, `GET /circles/:id.json` and `POST
1618
/circles.json` — land as `pings`. Moving a document, upload or folder
1719
between folders (`POST /recordings/:id/filing.json`, documented by bc3#13644 and
@@ -238,6 +240,7 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
238240
| vaults (move) | 1 | `files move` | ✅ | BC5 | - | `POST /recordings/:id/filing.json`: moves a document, upload or folder into another folder in the same project, in place (same id and comments). `--to` takes a folder ID or URL; `--position` is 1-indexed. Flat route, so no `--in` |
239241
| documents | 8 | `files`, `docs` | ✅ | BC4 | - | list, show, create, update. Create supports `--subscribe`/`--no-subscribe`, `--visible-to-clients` (root vault only) |
240242
| attachments | 1 | `uploads`, `attachments` | ✅ | BC4 | - | Upload via `attach`; list embedded attachments via `attachments list` (parses `<bc-attachment>` from content) |
243+
| embeds | 2 | `embeds` | ✅ | BC5 | - | create, show. Rich-text commands also embed a URL that stands alone on its line in Markdown content, through the same create |
241244
| **Schedule** |
242245
| calendars | 2 | `calendars` | ✅ | BC5 | - | show, update (color only). No index endpoint, so there is no `calendars list` — address one by id or pasted URL. The eleven colors are validated client-side for an immediate answer that names the alternatives; since v0.13.0 the SDK also carries the server's field-keyed 422 message |
243246
| schedules | 2 | `schedule` | ✅ | BC4 | - | Schedule container + settings |

‎e2e/smoke/run_smoke.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ level1=(
101101
"$SMOKE_DIR"/smoke_assign.bats
102102
"$SMOKE_DIR"/smoke_lineup.bats
103103
"$SMOKE_DIR"/smoke_communication_write.bats
104+
"$SMOKE_DIR"/smoke_embeds.bats
104105
"$SMOKE_DIR"/smoke_misc_write.bats
105106
"$SMOKE_DIR"/smoke_tools.bats
106107
"$SMOKE_DIR"/smoke_cards_column_write.bats

‎e2e/smoke/smoke_embeds.bats‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bats
2+
# smoke_embeds.bats - Level 1: Embeds for rich text
3+
4+
load smoke_helper
5+
6+
setup_file() {
7+
ensure_token || return 1
8+
}
9+
10+
@test "embeds without subcommand shows help" {
11+
run basecamp embeds
12+
assert_success
13+
assert_output_contains "create"
14+
}
15+
16+
@test "embeds create returns an embed with its sgid" {
17+
run_smoke basecamp embeds create "https://www.youtube.com/watch?v=dQw4w9WgXcQ" --json
18+
if [[ "$status" -ne 0 ]] && [[ "$output" == *"not found"* || "$output" == *"404"* ]]; then
19+
mark_unverifiable "This Basecamp does not serve POST /embeds.json to API clients yet"
20+
return
21+
fi
22+
assert_success
23+
assert_json_not_null '.data.sgid'
24+
echo "$output" | jq -r '.data.sgid' > "$BATS_FILE_TMPDIR/embed_sgid"
25+
}
26+
27+
@test "embeds show returns the embed" {
28+
[[ -s "$BATS_FILE_TMPDIR/embed_sgid" ]] || mark_unverifiable "No embed was created to show"
29+
run_smoke basecamp embeds show "$(cat "$BATS_FILE_TMPDIR/embed_sgid")" --json
30+
assert_success
31+
assert_json_value '.data.url' 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
32+
}

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
charm.land/bubbles/v2 v2.2.1
99
charm.land/bubbletea/v2 v2.1.0
1010
charm.land/lipgloss/v2 v2.0.6
11-
github.com/basecamp/basecamp-sdk/go v0.26.0
11+
github.com/basecamp/basecamp-sdk/go v0.27.0
1212
github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d
1313
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d
1414
github.com/basecamp/surfguard/go v0.1.0

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
8989
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
9090
github.com/basecamp/actioncable-go v1.2.0 h1:t0MxlfMTcmaGXZ5ztgKRs7ZiY1MwXtBRwSSSAh1ofVg=
9191
github.com/basecamp/actioncable-go v1.2.0/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs=
92-
github.com/basecamp/basecamp-sdk/go v0.26.0 h1:ENIK+0Q9WWRCzcJjz0RLLpvAMSfKoQOl8lHRumaEeOg=
93-
github.com/basecamp/basecamp-sdk/go v0.26.0/go.mod h1:PAp3gBnEPH7LtAyWXwgQ1tfmCAJiMkRHgRJAKT/Xefc=
92+
github.com/basecamp/basecamp-sdk/go v0.27.0 h1:Shq2lyAMBDvvBtmVLZZes42UyKkFMIvpR1Pa8xyWOVo=
93+
github.com/basecamp/basecamp-sdk/go v0.27.0/go.mod h1:mUm/MQGMyg1E1cddyxdG7tQHiq0NsYee2zUeAUT5VjU=
9494
github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d h1:jAzDrCCzDpIwhbFT1xVVs0z2xpXoDEkomHfKB2bUUp8=
9595
github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d/go.mod h1:iTBTaWvsPEFIcZfkxQHEfISyJ6sZ7036K6bNx0RY3EE=
9696
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4=

‎internal/cli/root.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ func Execute() {
362362
cmd.AddCommand(commands.NewProfileCmd())
363363
cmd.AddCommand(commands.NewSkillCmd())
364364
cmd.AddCommand(commands.NewAttachmentsCmd())
365+
cmd.AddCommand(commands.NewEmbedsCmd())
365366
cmd.AddCommand(commands.NewAttachCmd())
366367
cmd.AddCommand(commands.NewUploadCmd())
367368
cmd.AddCommand(commands.NewGaugesCmd())

‎internal/commands/commands.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func CommandCategories() []CommandCategory {
104104
{Name: "forwards", Category: "communication", Description: "Manage email forwards (inbox)", Actions: []string{"list", "show", "inbox", "replies", "reply"}},
105105
{Name: "subscriptions", Category: "communication", Description: "Manage notification subscriptions", Actions: []string{"show", "subscribe", "unsubscribe", "add", "remove"}},
106106
{Name: "attachments", Category: "communication", Description: "List and download attachments", Actions: []string{"list", "download"}},
107+
{Name: "embeds", Category: "communication", Description: "Embed videos, charts and other links in rich text", Actions: []string{"create", "show"}},
107108
{Name: "comments", Category: "communication", Description: "Manage comments", Actions: []string{"create", "list", "show", "thread", "update", "trash", "archive", "restore"}},
108109
{Name: "boost", Category: "communication", Description: "Manage boosts (reactions)", Actions: []string{"list", "show", "create", "update", "delete"}},
109110
{Name: "notifications", Category: "communication", Description: "View and manage notifications", Actions: []string{"list", "read", "bubbleups"}},

‎internal/commands/commands_test.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func buildRootWithAllCommands() *cobra.Command {
111111
root.AddCommand(commands.NewUpgradeCmd())
112112
root.AddCommand(commands.NewMigrateCmd())
113113
root.AddCommand(commands.NewAttachmentsCmd())
114+
root.AddCommand(commands.NewEmbedsCmd())
114115
root.AddCommand(commands.NewAttachCmd())
115116
root.AddCommand(commands.NewUploadCmd())
116117
root.AddCommand(commands.NewSkillCmd())

0 commit comments

Comments
 (0)