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
2 changes: 2 additions & 0 deletions API-COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github.

Out-of-scope sections are excluded from parity totals and scripts: chatbots (different auth), legacy Clientside (deprecated)

**SDK version:** v0.4.0 — uniform pagination (Limit/Page) on all List methods; `types.FlexibleTime` and `types.FlexInt` for wire format handling.

## Coverage by Section

| Section | Endpoints | CLI Command | Status | Priority | Notes |
Expand Down
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.0.0
charm.land/bubbletea/v2 v2.0.2
charm.land/lipgloss/v2 v2.0.0
github.com/basecamp/basecamp-sdk/go v0.3.1-0.20260310211211-4d7ec4217f2e
github.com/basecamp/basecamp-sdk/go v0.4.0
github.com/basecamp/cli v0.1.1
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/glamour v1.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6
github.com/aymanbagabas/go-udiff v0.4.0/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.3.1-0.20260310211211-4d7ec4217f2e h1:jmNmrnigG0vdvrDCfOsTYznhOYKtm+WBgcK8UU0lhFY=
github.com/basecamp/basecamp-sdk/go v0.3.1-0.20260310211211-4d7ec4217f2e/go.mod h1:g05DM58QkUm4/mvBAvRiugPw+F4trliuGkRGg8y+Th4=
github.com/basecamp/basecamp-sdk/go v0.4.0 h1:O/Sywalv97zHqaHboxuZo7JmMfwQvU3uuWEOMdZVChU=
github.com/basecamp/basecamp-sdk/go v0.4.0/go.mod h1:g05DM58QkUm4/mvBAvRiugPw+F4trliuGkRGg8y+Th4=
github.com/basecamp/cli v0.1.1 h1:FAF3M09xo1m7gJJXf38glCkT50ZUuvz+31f+c3R3zcc=
github.com/basecamp/cli v0.1.1/go.mod h1:NTHe+keCTGI2qM5sMXdkUN0QgU3zGbwnBxcmg8vD5QU=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/boost.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func runBoostList(cmd *cobra.Command, app *appctx.App, recording, project, event
return output.ErrUsage("Invalid event ID")
}

result, err := app.Account().Boosts().ListEvent(cmd.Context(), recordingIDInt, eventIDInt)
result, err := app.Account().Boosts().ListEvent(cmd.Context(), recordingIDInt, eventIDInt, nil)
if err != nil {
return convertSDKError(err)
}
Expand All @@ -133,7 +133,7 @@ func runBoostList(cmd *cobra.Command, app *appctx.App, recording, project, event
)
}

result, err := app.Account().Boosts().ListRecording(cmd.Context(), recordingIDInt)
result, err := app.Account().Boosts().ListRecording(cmd.Context(), recordingIDInt, nil)
if err != nil {
return convertSDKError(err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/campfire.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func newCampfireListCmd(project *string) *cobra.Command {
func runCampfireList(cmd *cobra.Command, app *appctx.App, project string, all bool) error {
// Account-wide campfire listing
if all {
result, err := app.Account().Campfires().List(cmd.Context())
result, err := app.Account().Campfires().List(cmd.Context(), nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -216,13 +216,13 @@ func runCampfireMessages(cmd *cobra.Command, app *appctx.App, campfireID, projec
}

// Get recent messages (lines) using SDK
result, err := app.Account().Campfires().ListLines(cmd.Context(), campfireIDInt)
result, err := app.Account().Campfires().ListLines(cmd.Context(), campfireIDInt, nil)
if err != nil {
return err
}
lines := result.Lines

// Take last N messages
// Take last N messages (newest)
if limit > 0 && len(lines) > limit {
lines = lines[len(lines)-limit:]
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/messagetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func runMessagetypesList(cmd *cobra.Command) error {
return err
}

typesResult, err := app.Account().MessageTypes().List(cmd.Context())
typesResult, err := app.Account().MessageTypes().List(cmd.Context(), nil)
if err != nil {
return convertSDKError(err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/people.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,14 +378,14 @@ func runPeoplePingable(cmd *cobra.Command, args []string) error {
return err
}

people, err := app.Account().People().Pingable(cmd.Context())
result, err := app.Account().People().Pingable(cmd.Context(), nil)
if err != nil {
return convertSDKError(err)
}

summary := fmt.Sprintf("%d pingable people", len(people))
summary := fmt.Sprintf("%d pingable people", len(result.People))

return app.OK(people, output.WithSummary(summary))
return app.OK(result.People, output.WithSummary(summary))
}

func newPeopleAddCmd() *cobra.Command {
Expand Down
19 changes: 8 additions & 11 deletions internal/commands/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,20 @@ Use 'basecamp search metadata' to see available search scopes.`,
}

// Build search options
var opts *basecamp.SearchOptions
opts := &basecamp.SearchOptions{}
if sortBy != "" {
opts = &basecamp.SearchOptions{
Sort: sortBy,
}
opts.Sort = sortBy
}
if limit > 0 {
opts.Limit = limit
}

results, err := app.Account().Search().Search(cmd.Context(), query, opts)
searchResult, err := app.Account().Search().Search(cmd.Context(), query, opts)
if err != nil {
return convertSDKError(err)
}

// Apply client-side limit if specified
if limit > 0 && len(results) > limit {
results = results[:limit]
}

results := searchResult.Results
summary := fmt.Sprintf("%d results for \"%s\"", len(results), query)

// Humanize for styled terminal output; preserve raw SDK structs
Expand All @@ -90,7 +87,7 @@ Use 'basecamp search metadata' to see available search scopes.`,
}

cmd.Flags().StringVarP(&sortBy, "sort", "s", "", "Sort by: created_at or updated_at (default: relevance)")
cmd.Flags().IntVarP(&limit, "limit", "n", 0, "Limit number of results")
cmd.Flags().IntVarP(&limit, "limit", "n", 0, "Maximum number of results to fetch (0 = all)")

cmd.AddCommand(newSearchMetadataCmd())

Expand Down
2 changes: 1 addition & 1 deletion internal/commands/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func runTemplatesList(cmd *cobra.Command, status string) error {
// SDK List() defaults to active status (API default)
// For archived/trashed, use raw API with status parameter
if status == "active" || status == "" {
templatesResult, err := app.Account().Templates().List(cmd.Context())
templatesResult, err := app.Account().Templates().List(cmd.Context(), nil)
if err != nil {
return convertSDKError(err)
}
Expand Down
32 changes: 20 additions & 12 deletions internal/commands/timeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ func runTimeline(cmd *cobra.Command, args []string, project, person string) erro
}

// Default: account-wide activity feed
events, err := app.Account().Timeline().Progress(cmd.Context())
result, err := app.Account().Timeline().Progress(cmd.Context(), nil)
if err != nil {
return convertSDKError(err)
}

return app.OK(events,
output.WithSummary(fmt.Sprintf("%d recent events", len(events))),
return app.OK(result.Events,
output.WithSummary(fmt.Sprintf("%d recent events", len(result.Events))),
output.WithBreadcrumbs(
output.Breadcrumb{
Action: "project",
Expand Down Expand Up @@ -131,17 +131,17 @@ func runProjectTimeline(cmd *cobra.Command, project string) error {
return output.ErrUsage("Invalid project ID")
}

events, err := app.Account().Timeline().ProjectTimeline(cmd.Context(), projectIDInt)
timelineResult, err := app.Account().Timeline().ProjectTimeline(cmd.Context(), projectIDInt, nil)
if err != nil {
return convertSDKError(err)
}

summary := fmt.Sprintf("%d events in %s", len(events), projectName)
summary := fmt.Sprintf("%d events in %s", len(timelineResult.Events), projectName)
if projectName == "" {
summary = fmt.Sprintf("%d events in project #%s", len(events), resolvedProjectID)
summary = fmt.Sprintf("%d events in project #%s", len(timelineResult.Events), resolvedProjectID)
}

return app.OK(events,
return app.OK(timelineResult.Events,
output.WithSummary(summary),
output.WithBreadcrumbs(
output.Breadcrumb{
Expand Down Expand Up @@ -172,7 +172,7 @@ func runPersonTimeline(cmd *cobra.Command, personArg string) error {
return output.ErrUsage("Invalid person ID")
}

result, err := app.Account().Timeline().PersonProgress(cmd.Context(), personID)
result, err := app.Account().Timeline().PersonProgress(cmd.Context(), personID, nil)
if err != nil {
return convertSDKError(err)
}
Expand Down Expand Up @@ -423,7 +423,7 @@ func runTimelineWatch(cmd *cobra.Command, args []string, project, person string,
}
description = "your activity"
fetchFn = func(ctx context.Context) ([]basecamp.TimelineEvent, error) {
result, err := app.Account().Timeline().PersonProgress(ctx, personID)
result, err := app.Account().Timeline().PersonProgress(ctx, personID, nil)
if err != nil {
return nil, err
}
Expand All @@ -441,7 +441,7 @@ func runTimelineWatch(cmd *cobra.Command, args []string, project, person string,
}
description = fmt.Sprintf("activity for %s", personName)
fetchFn = func(ctx context.Context) ([]basecamp.TimelineEvent, error) {
result, err := app.Account().Timeline().PersonProgress(ctx, personID)
result, err := app.Account().Timeline().PersonProgress(ctx, personID, nil)
if err != nil {
return nil, err
}
Expand All @@ -459,13 +459,21 @@ func runTimelineWatch(cmd *cobra.Command, args []string, project, person string,
}
description = fmt.Sprintf("activity in %s", projectName)
fetchFn = func(ctx context.Context) ([]basecamp.TimelineEvent, error) {
return app.Account().Timeline().ProjectTimeline(ctx, projectIDInt)
r, err := app.Account().Timeline().ProjectTimeline(ctx, projectIDInt, nil)
if err != nil {
return nil, err
}
return r.Events, nil
}
} else {
// Account-wide timeline
description = "account activity"
fetchFn = func(ctx context.Context) ([]basecamp.TimelineEvent, error) {
return app.Account().Timeline().Progress(ctx)
r, err := app.Account().Timeline().Progress(ctx, nil)
if err != nil {
return nil, err
}
return r.Events, nil
}
}

Expand Down
24 changes: 10 additions & 14 deletions internal/commands/timesheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func sumTimesheetHours(entries []basecamp.TimesheetEntry) float64 {
}

func newTimesheetProjectCmd(project *string) *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "project",
Short: "View project timesheet",
Long: "View timesheet entries for a project.",
Expand Down Expand Up @@ -159,15 +159,15 @@ func newTimesheetProjectCmd(project *string) *cobra.Command {
return output.ErrUsage("Invalid project ID")
}

entries, err := app.Account().Timesheet().ProjectReport(cmd.Context(), projectIDInt, nil)
result, err := app.Account().Timesheet().ProjectReport(cmd.Context(), projectIDInt, nil)
if err != nil {
return convertSDKError(err)
}

totalHours := sumTimesheetHours(entries)
totalHours := sumTimesheetHours(result.Entries)

return app.OK(entries,
output.WithSummary(fmt.Sprintf("%d entries (%.1fh total)", len(entries), totalHours)),
return app.OK(result.Entries,
output.WithSummary(fmt.Sprintf("%d entries (%.1fh total)", len(result.Entries), totalHours)),
output.WithBreadcrumbs(
output.Breadcrumb{
Action: "report",
Expand All @@ -183,12 +183,10 @@ func newTimesheetProjectCmd(project *string) *cobra.Command {
)
},
}

return cmd
}

func newTimesheetRecordingCmd(project *string) *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "item <id>",
Aliases: []string{"recording"},
Short: "View item timesheet",
Expand Down Expand Up @@ -222,15 +220,15 @@ func newTimesheetRecordingCmd(project *string) *cobra.Command {
return output.ErrUsage("Invalid ID")
}

entries, err := app.Account().Timesheet().RecordingReport(cmd.Context(), recordingID, nil)
result, err := app.Account().Timesheet().RecordingReport(cmd.Context(), recordingID, nil)
if err != nil {
return convertSDKError(err)
}

totalHours := sumTimesheetHours(entries)
totalHours := sumTimesheetHours(result.Entries)

return app.OK(entries,
output.WithSummary(fmt.Sprintf("%d entries (%.1fh total)", len(entries), totalHours)),
return app.OK(result.Entries,
output.WithSummary(fmt.Sprintf("%d entries (%.1fh total)", len(result.Entries), totalHours)),
output.WithBreadcrumbs(
output.Breadcrumb{
Action: "project",
Expand All @@ -246,6 +244,4 @@ func newTimesheetRecordingCmd(project *string) *cobra.Command {
)
},
}

return cmd
}
2 changes: 1 addition & 1 deletion internal/commands/todolistgroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func runTodolistgroupsList(cmd *cobra.Command, project, todolist string) error {
}

// Get groups via SDK
groupsResult, err := app.Account().TodolistGroups().List(cmd.Context(), todolistID)
groupsResult, err := app.Account().TodolistGroups().List(cmd.Context(), todolistID, nil)
if err != nil {
return convertSDKError(err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func runWebhooksList(cmd *cobra.Command, project *string) error {
return output.ErrUsage("Invalid project ID")
}

webhooksResult, err := app.Account().Webhooks().List(cmd.Context(), bucketID)
webhooksResult, err := app.Account().Webhooks().List(cmd.Context(), bucketID, nil)
if err != nil {
return convertSDKError(err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/names/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,13 +431,13 @@ func (r *Resolver) getPingable(ctx context.Context) ([]Person, error) {
return r.pingable, nil
}

sdkPeople, err := r.forAccount().People().Pingable(ctx)
sdkResult, err := r.forAccount().People().Pingable(ctx, nil)
if err != nil {
return nil, convertSDKError(err)
}

pingable := make([]Person, 0, len(sdkPeople))
for _, p := range sdkPeople {
pingable := make([]Person, 0, len(sdkResult.People))
for _, p := range sdkResult.People {
pingable = append(pingable, Person{
ID: p.ID,
Name: p.Name,
Expand Down
10 changes: 5 additions & 5 deletions internal/tui/workspace/data/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ func (h *Hub) CampfireLines(projectID, campfireID int64) *Pool[CampfireLinesResu
PollMax: 2 * time.Minute,
}, func(ctx context.Context) (CampfireLinesResult, error) {
client := h.accountClient()
result, err := client.Campfires().ListLines(ctx, campfireID)
result, err := client.Campfires().ListLines(ctx, campfireID, nil)
if err != nil {
return CampfireLinesResult{}, err
}
Expand Down Expand Up @@ -821,12 +821,12 @@ func (h *Hub) ProjectTimeline(projectID int64) *Pool[[]TimelineEventInfo] {
}, func(ctx context.Context) ([]TimelineEventInfo, error) {
client := h.accountClient()
acct := h.currentAccountInfo()
events, err := client.Timeline().ProjectTimeline(ctx, projectID)
result, err := client.Timeline().ProjectTimeline(ctx, projectID, nil)
if err != nil {
return nil, err
}
infos := make([]TimelineEventInfo, 0, len(events))
for _, e := range events {
infos := make([]TimelineEventInfo, 0, len(result.Events))
for _, e := range result.Events {
project := ""
var pID int64
if e.Bucket != nil {
Expand Down Expand Up @@ -870,7 +870,7 @@ func (h *Hub) Boosts(projectID, recordingID int64) *Pool[BoostSummary] {
p := RealmPool(realm, key, func() *Pool[BoostSummary] {
return NewPool(key, PoolConfig{}, func(ctx context.Context) (BoostSummary, error) {
client := h.accountClient()
result, err := client.Boosts().ListRecording(ctx, recordingID)
result, err := client.Boosts().ListRecording(ctx, recordingID, nil)
if err != nil {
return BoostSummary{}, err
}
Expand Down
Loading
Loading