From fb3460a0a4e76c9673346107680bf04ed62ff9c5 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 25 Mar 2026 00:30:45 -0700 Subject: [PATCH 1/6] Bump basecamp-sdk to v0.7.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes Person.Id deserialization (normalizeJSON pre-unmarshal for notification responses) and Account.Logo type (string → AccountLogo object). Adapts two breaking changes: Cards().Move() and Todos().Reposition() now take optional params. Adds AVIF/HEIC to richtext MIME extension map for logo upload support. --- go.mod | 4 ++-- go.sum | 8 ++++---- internal/commands/cards.go | 4 ++-- internal/commands/todos.go | 2 +- internal/richtext/mime.go | 2 ++ internal/tui/workspace/data/mutations_card.go | 2 +- internal/version/sdk-provenance.json | 8 ++++---- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index ace5a0ed3..98d1b8fc1 100644 --- a/go.mod +++ b/go.mod @@ -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.2 - github.com/basecamp/basecamp-sdk/go v0.6.1-0.20260320103941-fe6154c8b320 + github.com/basecamp/basecamp-sdk/go v0.7.1 github.com/basecamp/cli v0.2.1 github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/glamour v1.0.0 @@ -65,7 +65,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect - github.com/oapi-codegen/runtime v1.2.0 // indirect + github.com/oapi-codegen/runtime v1.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/go.sum b/go.sum index 31a8e4688..c4b70ca18 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,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.6.1-0.20260320103941-fe6154c8b320 h1:xHbqMcj57XgSb69wsy+dhTzZ+p3IYb/BnjisI9NF5c4= -github.com/basecamp/basecamp-sdk/go v0.6.1-0.20260320103941-fe6154c8b320/go.mod h1:g05DM58QkUm4/mvBAvRiugPw+F4trliuGkRGg8y+Th4= +github.com/basecamp/basecamp-sdk/go v0.7.1 h1:GsQl8tEImbPMZZR8gtWQkP63a1j/7H1S6K2Zw8XZpbU= +github.com/basecamp/basecamp-sdk/go v0.7.1/go.mod h1:3MczpIvzsnslxVqb3TVrqVe8u4Jz/RyjIsTldhmiK5Q= github.com/basecamp/cli v0.2.1 h1:8GyehPVtsTXla0oOPu4QgXRjwwzJ99prlByvyi+0HRQ= github.com/basecamp/cli v0.2.1/go.mod h1:p8tt/DatJ2LAzWO6N6tNfV8x3gF5T3IxDTo+U8FfWPo= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -129,8 +129,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= -github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= +github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/internal/commands/cards.go b/internal/commands/cards.go index 4e00a9737..4b8badcb7 100644 --- a/internal/commands/cards.go +++ b/internal/commands/cards.go @@ -784,7 +784,7 @@ You can pass either a card ID or a Basecamp URL: Position: position, }) } else { - err = app.Account().Cards().Move(cmd.Context(), cardID, columnID) + err = app.Account().Cards().Move(cmd.Context(), cardID, columnID, nil) } if err != nil { return convertSDKError(err) @@ -895,7 +895,7 @@ func moveCardOnHold(cmd *cobra.Command, app *appctx.App, cardID int64, cardIDStr ) } - err := app.Account().Cards().Move(cmd.Context(), cardID, column.OnHold.ID) + err := app.Account().Cards().Move(cmd.Context(), cardID, column.OnHold.ID, nil) if err != nil { return convertSDKError(err) } diff --git a/internal/commands/todos.go b/internal/commands/todos.go index 3a2a363fa..0d4947b5b 100644 --- a/internal/commands/todos.go +++ b/internal/commands/todos.go @@ -1677,7 +1677,7 @@ You can pass either a todo ID or a Basecamp URL: return output.ErrUsage("Invalid todo ID") } - err = app.Account().Todos().Reposition(cmd.Context(), todoID, position) + err = app.Account().Todos().Reposition(cmd.Context(), todoID, position, nil) if err != nil { return convertSDKError(err) } diff --git a/internal/richtext/mime.go b/internal/richtext/mime.go index 31519e9b5..22dad5cfb 100644 --- a/internal/richtext/mime.go +++ b/internal/richtext/mime.go @@ -29,6 +29,8 @@ var mimeByExt = map[string]string{ ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", + ".avif": "image/avif", + ".heic": "image/heic", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".mp4": "video/mp4", diff --git a/internal/tui/workspace/data/mutations_card.go b/internal/tui/workspace/data/mutations_card.go index 873daadbc..b7b130b62 100644 --- a/internal/tui/workspace/data/mutations_card.go +++ b/internal/tui/workspace/data/mutations_card.go @@ -64,7 +64,7 @@ func (m CardMoveMutation) ApplyLocally(columns []CardColumnInfo) []CardColumnInf // ApplyRemotely calls the SDK to move the card. func (m CardMoveMutation) ApplyRemotely(ctx context.Context) error { - return m.Client.Cards().Move(ctx, m.CardID, m.TargetColumnID) + return m.Client.Cards().Move(ctx, m.CardID, m.TargetColumnID, nil) } // IsReflectedIn returns true when the card appears in the target column diff --git a/internal/version/sdk-provenance.json b/internal/version/sdk-provenance.json index 4a8f619d5..d019f8477 100644 --- a/internal/version/sdk-provenance.json +++ b/internal/version/sdk-provenance.json @@ -1,13 +1,13 @@ { "sdk": { "module": "github.com/basecamp/basecamp-sdk/go", - "version": "v0.6.1-0.20260320103941-fe6154c8b320", - "revision": "fe6154c8b320", - "updated_at": "2026-03-20T10:39:41Z" + "version": "v0.7.1", + "revision": "55ea29ab3388", + "updated_at": "2026-03-25T07:00:09Z" }, "api": { "repo": "basecamp/bc3", - "revision": "d57b6994a74f123aaf1fd685f993befb28a012fd", + "revision": "056a356ee0d3b018362adbc8b44703df0567adbf", "synced_at": "" } } From fca9c95fade7420db73a10594d165de09c2cb456 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 25 Mar 2026 00:30:45 -0700 Subject: [PATCH 2/6] Merge upstream fixes (#363, #364, #365, #367, #369) Also fixes: chat upload error now wraps converted SDK error instead of raw error; schedule update --attach without --description now fetches current description to append rather than replace. --- internal/commands/chat.go | 2 +- internal/commands/schedule.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/commands/chat.go b/internal/commands/chat.go index d6d4a9366..5a7eca520 100644 --- a/internal/commands/chat.go +++ b/internal/commands/chat.go @@ -425,7 +425,7 @@ func runChatPost(cmd *cobra.Command, app *appctx.App, chatID, project, content, outErr.Message = fmt.Sprintf("%s: %s", filePath, outErr.Message) return outErr } - return fmt.Errorf("%s: %w", filePath, err) + return fmt.Errorf("%s: %w", filePath, sdkErr) } uploadIDs = append(uploadIDs, uploadLine.ID) } diff --git a/internal/commands/schedule.go b/internal/commands/schedule.go index 1ee087fe0..f22b2a015 100644 --- a/internal/commands/schedule.go +++ b/internal/commands/schedule.go @@ -677,13 +677,18 @@ You can pass either an entry ID or a Basecamp URL: } } - // Handle attachments when no description text provided + // Handle attachments when no description text provided — + // fetch the current description so we append rather than replace if description == "" && len(attachFiles) > 0 { refs, attachErr := uploadAttachments(cmd, app, attachFiles) if attachErr != nil { return attachErr } - req.Description = richtext.EmbedAttachments("", refs) + existing, fetchErr := app.Account().Schedules().GetEntry(cmd.Context(), entryIDInt) + if fetchErr != nil { + return convertSDKError(fetchErr) + } + req.Description = richtext.EmbedAttachments(existing.Description, refs) hasChanges = true } From 0be714a64ecec44c861caf6511116d1aea534c11 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 25 Mar 2026 00:30:46 -0700 Subject: [PATCH 3/6] Add gauges command for project progress tracking Subcommands: list (account-wide), needles/needle (project-scoped), create/update/delete, enable/disable. Create validates --position (0-100), --color, --notify (requires --subscriptions for custom mode). --- e2e/smoke/smoke_gauges.bats | 58 +++++ internal/commands/gauges.go | 427 ++++++++++++++++++++++++++++++++++++ 2 files changed, 485 insertions(+) create mode 100644 e2e/smoke/smoke_gauges.bats create mode 100644 internal/commands/gauges.go diff --git a/e2e/smoke/smoke_gauges.bats b/e2e/smoke/smoke_gauges.bats new file mode 100644 index 000000000..711c6027a --- /dev/null +++ b/e2e/smoke/smoke_gauges.bats @@ -0,0 +1,58 @@ +#!/usr/bin/env bats +# smoke_gauges.bats - Level 0: Gauge operations + +load smoke_helper + +setup_file() { + ensure_token || return 1 +} + +@test "gauges list returns gauges" { + run_smoke basecamp gauges list --json + assert_success + assert_json_value '.ok' 'true' +} + +@test "gauges needles requires project" { + ensure_project || mark_unverifiable "Cannot discover project" + run_smoke basecamp gauges needles -p "$QA_PROJECT" --json + # Gauges may not be enabled on the project (403) + [[ "$status" -eq 4 ]] && mark_unverifiable "Gauges not enabled on project" + assert_success + assert_json_value '.ok' 'true' +} + +@test "gauges needle requires valid ID" { + run_smoke basecamp gauges needle 999999 --json + mark_unverifiable "Requires valid needle ID" +} + +@test "gauges create requires project and position" { + ensure_project || mark_unverifiable "Cannot discover project" + run_smoke basecamp gauges create --position 50 -p "$QA_PROJECT" --json + # May fail if gauges not enabled (403) or other constraint + [[ "$status" -eq 4 ]] && mark_unverifiable "Gauges not enabled on project" + mark_unverifiable "Mutating test - gauge creation may not be safe in all environments" +} + +@test "gauges update requires valid needle ID" { + run_smoke basecamp gauges update 999999 --description "test" --json + mark_unverifiable "Requires valid needle ID" +} + +@test "gauges delete requires valid needle ID" { + run_smoke basecamp gauges delete 999999 --json + mark_unverifiable "Requires valid needle ID" +} + +@test "gauges enable requires project" { + ensure_project || mark_unverifiable "Cannot discover project" + run_smoke basecamp gauges enable -p "$QA_PROJECT" --json + mark_unverifiable "Mutating test - gauge toggle may not be safe in all environments" +} + +@test "gauges disable requires project" { + ensure_project || mark_unverifiable "Cannot discover project" + run_smoke basecamp gauges disable -p "$QA_PROJECT" --json + mark_unverifiable "Mutating test - gauge toggle may not be safe in all environments" +} diff --git a/internal/commands/gauges.go b/internal/commands/gauges.go new file mode 100644 index 000000000..ff23c4b5a --- /dev/null +++ b/internal/commands/gauges.go @@ -0,0 +1,427 @@ +package commands + +import ( + "fmt" + "strconv" + + "github.com/basecamp/basecamp-sdk/go/pkg/basecamp" + "github.com/spf13/cobra" + + "github.com/basecamp/basecamp-cli/internal/appctx" + "github.com/basecamp/basecamp-cli/internal/output" +) + +// NewGaugesCmd creates the gauges command group. +func NewGaugesCmd() *cobra.Command { + var project string + + cmd := &cobra.Command{ + Use: "gauges", + Short: "Manage gauges", + Long: `Manage gauges for tracking project progress. + +A gauge shows the current state of a project via needles positioned on a +0–100 scale, each colored green, yellow, or red.`, + Annotations: map[string]string{ + "agent_notes": "Gauges track project progress with colored needles (0-100 scale).\n" + + "list is account-wide. needles/create/enable/disable are project-scoped (--in).\n" + + "Colors: green, yellow, red. Notify: everyone, working_on, custom.", + }, + } + + cmd.PersistentFlags().StringVarP(&project, "project", "p", "", "Project ID or name") + cmd.PersistentFlags().StringVar(&project, "in", "", "Project ID (alias for --project)") + + cmd.AddCommand( + newGaugesListCmd(), + newGaugesNeedlesCmd(&project), + newGaugesNeedleCmd(), + newGaugesCreateCmd(&project), + newGaugesUpdateCmd(), + newGaugesDeleteCmd(), + newGaugesEnableCmd(&project), + newGaugesDisableCmd(&project), + ) + + return cmd +} + +func newGaugesListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all gauges across the account", + Long: "List all gauges across every project in the account.", + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + gauges, err := app.Account().Gauges().List(cmd.Context()) + if err != nil { + return convertSDKError(err) + } + + return app.OK(gauges, + output.WithSummary(fmt.Sprintf("%d gauge(s)", len(gauges))), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "needles", + Cmd: "basecamp gauges needles --in ", + Description: "View needles for a project", + }, + ), + ) + }, + } +} + +func newGaugesNeedlesCmd(project *string) *cobra.Command { + return &cobra.Command{ + Use: "needles", + Short: "List needles for a project's gauge", + Long: `List all needles on a project's gauge. + + basecamp gauges needles --in MyProject`, + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + resolvedProjectID, err := resolveProjectID(cmd, app, *project) + if err != nil { + return err + } + + projectID, err := strconv.ParseInt(resolvedProjectID, 10, 64) + if err != nil { + return output.ErrUsage("Invalid project ID") + } + + needles, err := app.Account().Gauges().ListNeedles(cmd.Context(), projectID) + if err != nil { + return convertSDKError(err) + } + + return app.OK(needles, + output.WithSummary(fmt.Sprintf("%d needle(s)", len(needles))), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "create", + Cmd: fmt.Sprintf("basecamp gauges create --position 50 --in %s", resolvedProjectID), + Description: "Add a needle", + }, + ), + ) + }, + } +} + +func newGaugesNeedleCmd() *cobra.Command { + return &cobra.Command{ + Use: "needle ", + Short: "Show a needle", + Long: "Show details for a specific gauge needle.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + needleID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return output.ErrUsage("Invalid needle ID") + } + + needle, err := app.Account().Gauges().GetNeedle(cmd.Context(), needleID) + if err != nil { + return convertSDKError(err) + } + + return app.OK(needle, + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "update", + Cmd: fmt.Sprintf("basecamp gauges update %d --description '...'", needleID), + Description: "Update needle", + }, + output.Breadcrumb{ + Action: "delete", + Cmd: fmt.Sprintf("basecamp gauges delete %d", needleID), + Description: "Delete needle", + }, + ), + ) + }, + } +} + +func newGaugesCreateCmd(project *string) *cobra.Command { + var position int32 + var color string + var description string + var notify string + var subscriptions []int64 + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a gauge needle", + Long: `Create a new needle on a project's gauge. + + basecamp gauges create --position 75 --color green --in MyProject + basecamp gauges create --position 50 --color yellow --description "Halfway there" --in MyProject`, + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + resolvedProjectID, err := resolveProjectID(cmd, app, *project) + if err != nil { + return err + } + + projectID, err := strconv.ParseInt(resolvedProjectID, 10, 64) + if err != nil { + return output.ErrUsage("Invalid project ID") + } + + if !cmd.Flags().Changed("position") { + return output.ErrUsage("--position is required") + } + if position < 0 || position > 100 { + return output.ErrUsage("--position must be between 0 and 100") + } + + req := &basecamp.CreateGaugeNeedleRequest{ + Position: position, + } + if color != "" { + req.Color = color + } + if description != "" { + req.Description = description + } + if notify != "" { + req.Notify = notify + if notify == "custom" { + if len(subscriptions) == 0 { + return output.ErrUsage("--subscriptions required when using --notify custom") + } + req.Subscriptions = subscriptions + } + } + + needle, err := app.Account().Gauges().CreateNeedle(cmd.Context(), projectID, req) + if err != nil { + return convertSDKError(err) + } + + return app.OK(needle, + output.WithSummary(fmt.Sprintf("Created needle #%d at position %d", needle.ID, needle.Position)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "needles", + Cmd: fmt.Sprintf("basecamp gauges needles --in %s", resolvedProjectID), + Description: "View all needles", + }, + ), + ) + }, + } + + cmd.Flags().Int32Var(&position, "position", 0, "Position on gauge (0-100, required)") + cmd.Flags().StringVar(&color, "color", "", "Needle color: green, yellow, or red") + cmd.Flags().StringVar(&description, "description", "", "Description (rich text HTML)") + cmd.Flags().StringVar(¬ify, "notify", "", "Notification mode: everyone, working_on, or custom") + cmd.Flags().Int64SliceVar(&subscriptions, "subscriptions", nil, "Person IDs to notify (used with --notify custom)") + + return cmd +} + +func newGaugesUpdateCmd() *cobra.Command { + var description string + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a gauge needle", + Long: `Update a needle's description. + + basecamp gauges update 12345 --description "Updated status"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + needleID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return output.ErrUsage("Invalid needle ID") + } + + if !cmd.Flags().Changed("description") { + return output.ErrUsage("No changes specified (use --description)") + } + + req := &basecamp.UpdateGaugeNeedleRequest{ + Description: description, + } + + needle, err := app.Account().Gauges().UpdateNeedle(cmd.Context(), needleID, req) + if err != nil { + return convertSDKError(err) + } + + return app.OK(needle, + output.WithSummary(fmt.Sprintf("Updated needle #%d", needleID)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "view", + Cmd: fmt.Sprintf("basecamp gauges needle %d", needleID), + Description: "View needle", + }, + ), + ) + }, + } + + cmd.Flags().StringVar(&description, "description", "", "New description (rich text HTML)") + + return cmd +} + +func newGaugesDeleteCmd() *cobra.Command { + return &cobra.Command{ + Use: "delete ", + Short: "Delete a gauge needle", + Long: "Delete a needle from a project's gauge.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + needleID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return output.ErrUsage("Invalid needle ID") + } + + err = app.Account().Gauges().DestroyNeedle(cmd.Context(), needleID) + if err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"deleted": true, "id": needleID}, + output.WithSummary(fmt.Sprintf("Deleted needle #%d", needleID)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "list", + Cmd: "basecamp gauges list", + Description: "View all gauges", + }, + ), + ) + }, + } +} + +func newGaugesEnableCmd(project *string) *cobra.Command { + return &cobra.Command{ + Use: "enable", + Short: "Enable gauge on a project", + Long: `Enable the gauge tool on a project. + + basecamp gauges enable --in MyProject`, + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + resolvedProjectID, err := resolveProjectID(cmd, app, *project) + if err != nil { + return err + } + + projectID, err := strconv.ParseInt(resolvedProjectID, 10, 64) + if err != nil { + return output.ErrUsage("Invalid project ID") + } + + err = app.Account().Gauges().Toggle(cmd.Context(), projectID, true) + if err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"enabled": true}, + output.WithSummary(fmt.Sprintf("Gauge enabled on project %s", resolvedProjectID)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "needles", + Cmd: fmt.Sprintf("basecamp gauges needles --in %s", resolvedProjectID), + Description: "View needles", + }, + output.Breadcrumb{ + Action: "create", + Cmd: fmt.Sprintf("basecamp gauges create --position 50 --in %s", resolvedProjectID), + Description: "Add a needle", + }, + ), + ) + }, + } +} + +func newGaugesDisableCmd(project *string) *cobra.Command { + return &cobra.Command{ + Use: "disable", + Short: "Disable gauge on a project", + Long: `Disable the gauge tool on a project. + +The gauge is not deleted — just hidden. Use 'basecamp gauges enable' to restore.`, + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + resolvedProjectID, err := resolveProjectID(cmd, app, *project) + if err != nil { + return err + } + + projectID, err := strconv.ParseInt(resolvedProjectID, 10, 64) + if err != nil { + return output.ErrUsage("Invalid project ID") + } + + err = app.Account().Gauges().Toggle(cmd.Context(), projectID, false) + if err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"enabled": false}, + output.WithSummary(fmt.Sprintf("Gauge disabled on project %s", resolvedProjectID)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "enable", + Cmd: fmt.Sprintf("basecamp gauges enable --in %s", resolvedProjectID), + Description: "Re-enable gauge", + }, + ), + ) + }, + } +} From 1eb44bf7368640dcadbc383e8cd539db0a2f989a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 25 Mar 2026 00:30:47 -0700 Subject: [PATCH 4/6] Add assignments command for viewing my assignments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subcommands: list, completed, due (with scope filter). Shortcut pattern — bare invocation shows priorities + non-priorities. --- e2e/smoke/smoke_assignments.bats | 26 +++++ internal/commands/assignments.go | 162 +++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 e2e/smoke/smoke_assignments.bats create mode 100644 internal/commands/assignments.go diff --git a/e2e/smoke/smoke_assignments.bats b/e2e/smoke/smoke_assignments.bats new file mode 100644 index 000000000..96c5c460d --- /dev/null +++ b/e2e/smoke/smoke_assignments.bats @@ -0,0 +1,26 @@ +#!/usr/bin/env bats +# smoke_assignments.bats - Level 0: My assignments operations + +load smoke_helper + +setup_file() { + ensure_token || return 1 +} + +@test "assignments list returns assignments" { + run_smoke basecamp assignments list --json + assert_success + assert_json_value '.ok' 'true' +} + +@test "assignments completed returns completed items" { + run_smoke basecamp assignments completed --json + assert_success + assert_json_value '.ok' 'true' +} + +@test "assignments due returns due items" { + run_smoke basecamp assignments due overdue --json + assert_success + assert_json_value '.ok' 'true' +} diff --git a/internal/commands/assignments.go b/internal/commands/assignments.go new file mode 100644 index 000000000..068c79d3f --- /dev/null +++ b/internal/commands/assignments.go @@ -0,0 +1,162 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/basecamp/basecamp-cli/internal/appctx" + "github.com/basecamp/basecamp-cli/internal/output" +) + +// NewAssignmentsCmd creates the assignments command. +func NewAssignmentsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "assignments", + Short: "View my assignments", + Long: `View your current assignments across all projects. + +Shows both priority and non-priority items. Use subcommands to filter +by completion status or due date.`, + Annotations: map[string]string{ + "agent_notes": "Account-wide — no --in needed.\n" + + "Shows priorities and non-priorities.\n" + + "Use 'due overdue' for overdue items, 'completed' for done items.", + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runAssignmentsList(cmd) + }, + } + + cmd.AddCommand( + newAssignmentsListCmd(), + newAssignmentsCompletedCmd(), + newAssignmentsDueCmd(), + ) + + return cmd +} + +func newAssignmentsListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List current assignments", + Long: "List all current assignments (same as bare 'assignments').", + RunE: func(cmd *cobra.Command, args []string) error { + return runAssignmentsList(cmd) + }, + } +} + +func runAssignmentsList(cmd *cobra.Command) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + result, err := app.Account().MyAssignments().Get(cmd.Context()) + if err != nil { + return convertSDKError(err) + } + + total := len(result.Priorities) + len(result.NonPriorities) + summary := fmt.Sprintf("%d assignment(s)", total) + if len(result.Priorities) > 0 { + summary += fmt.Sprintf(" (%d priority)", len(result.Priorities)) + } + + return app.OK(result, + output.WithSummary(summary), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "completed", + Cmd: "basecamp assignments completed", + Description: "View completed assignments", + }, + output.Breadcrumb{ + Action: "due", + Cmd: "basecamp assignments due overdue", + Description: "View overdue assignments", + }, + ), + ) +} + +func newAssignmentsCompletedCmd() *cobra.Command { + return &cobra.Command{ + Use: "completed", + Short: "View completed assignments", + Long: "View your recently completed assignments.", + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + items, err := app.Account().MyAssignments().Completed(cmd.Context()) + if err != nil { + return convertSDKError(err) + } + + return app.OK(items, + output.WithSummary(fmt.Sprintf("%d completed assignment(s)", len(items))), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "current", + Cmd: "basecamp assignments", + Description: "View current assignments", + }, + ), + ) + }, + } +} + +func newAssignmentsDueCmd() *cobra.Command { + return &cobra.Command{ + Use: "due [scope]", + Short: "View assignments by due date", + Long: `View assignments filtered by due date scope. + +Scopes: overdue, due_today, due_tomorrow, due_later_this_week, due_next_week, due_later. + + basecamp assignments due overdue + basecamp assignments due due_today`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + scope := "" + if len(args) > 0 { + scope = args[0] + } + + items, err := app.Account().MyAssignments().Due(cmd.Context(), scope) + if err != nil { + return convertSDKError(err) + } + + label := "due assignment(s)" + if scope != "" { + label = scope + " assignment(s)" + } + + return app.OK(items, + output.WithSummary(fmt.Sprintf("%d %s", len(items), label)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "all", + Cmd: "basecamp assignments", + Description: "View all assignments", + }, + ), + ) + }, + } +} From e48a6aaa720911f640ce4a3947595e66245e0b75 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 25 Mar 2026 00:30:47 -0700 Subject: [PATCH 5/6] Add notifications command, extend accounts, register all new commands Notifications: list (paginated with dynamic next-page breadcrumb) and read (resolves IDs to SGIDs from --page). Accounts: show, update, logo upload (5MB + image type validation) and remove. Registers all new commands in root.go, catalog, and test harness. --- e2e/smoke/smoke_account.bats | 30 +++++ e2e/smoke/smoke_notifications.bats | 20 +++ internal/cli/root.go | 3 + internal/commands/accounts.go | 203 +++++++++++++++++++++++++++++ internal/commands/commands.go | 5 +- internal/commands/commands_test.go | 3 + internal/commands/notifications.go | 195 +++++++++++++++++++++++++++ scripts/check-bare-groups.sh | 6 +- 8 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 e2e/smoke/smoke_notifications.bats create mode 100644 internal/commands/notifications.go diff --git a/e2e/smoke/smoke_account.bats b/e2e/smoke/smoke_account.bats index a335858b2..192389e78 100644 --- a/e2e/smoke/smoke_account.bats +++ b/e2e/smoke/smoke_account.bats @@ -52,3 +52,33 @@ setup_file() { assert_success assert_json_value '.ok' 'true' } + +# --- Account management --- + +@test "accounts show returns account details" { + run_smoke basecamp accounts show --json + assert_success + assert_json_value '.ok' 'true' +} + +@test "accounts update rejects bare invocation" { + run_smoke basecamp accounts update --json + assert_failure + assert_output_contains "No changes specified" +} + +@test "accounts logo upload rejects invalid file" { + run_smoke basecamp accounts logo upload /dev/null --json + assert_failure +} + +@test "accounts logo remove deletes logo" { + mark_unverifiable "Mutating test - logo removal not safe in smoke environment" + run_smoke basecamp accounts logo remove --json +} + +# --- account alias (covered by accounts tests above) --- + +@test "account is out of scope" { + mark_out_of_scope "Alias for accounts — tested via canonical form" +} diff --git a/e2e/smoke/smoke_notifications.bats b/e2e/smoke/smoke_notifications.bats new file mode 100644 index 000000000..d7d14ff69 --- /dev/null +++ b/e2e/smoke/smoke_notifications.bats @@ -0,0 +1,20 @@ +#!/usr/bin/env bats +# smoke_notifications.bats - Level 0: Notification operations + +load smoke_helper + +setup_file() { + ensure_token || return 1 +} + +@test "notifications list returns notifications" { + run_smoke basecamp notifications list --json + assert_success + assert_json_value '.ok' 'true' +} + +@test "notifications read rejects unknown ID" { + run_smoke basecamp notifications read 999999 --json + assert_failure + assert_output_contains "not found" +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 7c790b7d8..e38a56804 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -314,6 +314,9 @@ func Execute() { cmd.AddCommand(commands.NewAttachmentsCmd()) cmd.AddCommand(commands.NewAttachCmd()) cmd.AddCommand(commands.NewUploadCmd()) + cmd.AddCommand(commands.NewGaugesCmd()) + cmd.AddCommand(commands.NewAssignmentsCmd()) + cmd.AddCommand(commands.NewNotificationsCmd()) cmd.AddCommand(commands.NewTUICmd()) cmd.AddCommand(commands.NewBonfireCmd()) diff --git a/internal/commands/accounts.go b/internal/commands/accounts.go index 8aa0fd418..4f1001e4e 100644 --- a/internal/commands/accounts.go +++ b/internal/commands/accounts.go @@ -2,12 +2,15 @@ package commands import ( "fmt" + "os" + "path/filepath" "strconv" "github.com/spf13/cobra" "github.com/basecamp/basecamp-cli/internal/appctx" "github.com/basecamp/basecamp-cli/internal/output" + "github.com/basecamp/basecamp-cli/internal/richtext" "github.com/basecamp/basecamp-cli/internal/tui/resolve" ) @@ -23,6 +26,9 @@ func NewAccountsCmd() *cobra.Command { cmd.AddCommand( newAccountsListCmd(), newAccountsUseCmd(), + newAccountsShowCmd(), + newAccountsUpdateCmd(), + newAccountsLogoCmd(), ) return cmd @@ -161,3 +167,200 @@ func newAccountsUseCmd() *cobra.Command { return cmd } + +func newAccountsShowCmd() *cobra.Command { + return &cobra.Command{ + Use: "show", + Short: "Show account details", + Long: "Show details for the current account including limits, settings, and subscription.", + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + account, err := app.Account().Account().GetAccount(cmd.Context()) + if err != nil { + return convertSDKError(err) + } + + return app.OK(account, + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "update", + Cmd: "basecamp accounts update --name '...'", + Description: "Rename account", + }, + output.Breadcrumb{ + Action: "list", + Cmd: "basecamp accounts list", + Description: "List accounts", + }, + ), + ) + }, + } +} + +func newAccountsUpdateCmd() *cobra.Command { + var name string + + cmd := &cobra.Command{ + Use: "update", + Short: "Update account settings", + Long: `Update account settings. + + basecamp accounts update --name "New Company Name"`, + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + if !cmd.Flags().Changed("name") { + return output.ErrUsage("No changes specified (use --name)") + } + + account, err := app.Account().Account().UpdateName(cmd.Context(), name) + if err != nil { + return convertSDKError(err) + } + + return app.OK(account, + output.WithSummary(fmt.Sprintf("Account renamed to %q", account.Name)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "show", + Cmd: "basecamp accounts show", + Description: "View account", + }, + ), + ) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "New account name") + + return cmd +} + +func newAccountsLogoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "logo", + Short: "Manage account logo", + Long: "Upload or remove the account logo.", + } + + cmd.AddCommand( + newAccountsLogoUploadCmd(), + newAccountsLogoRemoveCmd(), + ) + + return cmd +} + +func newAccountsLogoUploadCmd() *cobra.Command { + return &cobra.Command{ + Use: "upload ", + Short: "Upload account logo", + Long: `Upload or replace the account logo. + +Accepts PNG, JPEG, GIF, WebP, AVIF, or HEIC files up to 5 MB. + + basecamp accounts logo upload logo.png`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + path := richtext.NormalizeDragPath(args[0]) + + if err := richtext.ValidateFile(path); err != nil { + return fmt.Errorf("%s: %w", args[0], err) + } + + // Enforce logo-specific constraints (5 MB, image types only) + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("%s: %w", args[0], err) + } + const maxLogoSize = 5 * 1024 * 1024 + if info.Size() > maxLogoSize { + return output.ErrUsage(fmt.Sprintf("%s exceeds maximum logo size of 5 MB", filepath.Base(path))) + } + + contentType := richtext.DetectMIME(path) + switch contentType { + case "image/png", "image/jpeg", "image/gif", "image/webp", "image/avif", "image/heic": + // OK + default: + return output.ErrUsage(fmt.Sprintf("%s: unsupported image type %s (use PNG, JPEG, GIF, WebP, AVIF, or HEIC)", filepath.Base(path), contentType)) + } + + filename := filepath.Base(path) + + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("%s: %w", args[0], err) + } + defer f.Close() + + err = app.Account().Account().UpdateLogo(cmd.Context(), f, filename, contentType) + if err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"uploaded": true, "filename": filename}, + output.WithSummary(fmt.Sprintf("Logo uploaded: %s", filename)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "show", + Cmd: "basecamp accounts show", + Description: "View account", + }, + output.Breadcrumb{ + Action: "remove", + Cmd: "basecamp accounts logo remove", + Description: "Remove logo", + }, + ), + ) + }, + } +} + +func newAccountsLogoRemoveCmd() *cobra.Command { + return &cobra.Command{ + Use: "remove", + Short: "Remove account logo", + Long: "Remove the account logo.", + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + err := app.Account().Account().RemoveLogo(cmd.Context()) + if err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"removed": true}, + output.WithSummary("Logo removed"), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "upload", + Cmd: "basecamp accounts logo upload ", + Description: "Upload a new logo", + }, + ), + ) + }, + } +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 19aaacb0c..632c8a457 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -39,6 +39,7 @@ func CommandCategories() []CommandCategory { {Name: "todolists", Category: "core", Description: "Manage to-do lists", Actions: []string{"list", "show", "create", "update", "trash", "archive", "restore"}}, {Name: "todosets", Category: "core", Description: "Manage to-do set containers", Actions: []string{"list", "show"}}, {Name: "hillcharts", Category: "core", Description: "Manage hill charts", Actions: []string{"show", "track", "untrack"}}, + {Name: "gauges", Category: "core", Description: "Manage gauges", Actions: []string{"list", "needles", "needle", "create", "update", "delete", "enable", "disable"}}, {Name: "todolistgroups", Category: "core", Description: "Manage to-do list groups", Actions: []string{"list", "show", "create", "update", "position"}}, {Name: "messages", Category: "core", Description: "Manage messages", Actions: []string{"list", "show", "create", "update", "publish", "pin", "unpin", "trash", "archive", "restore"}}, {Name: "chat", Category: "core", Description: "Chat in real-time", Actions: []string{"list", "messages", "post", "upload", "line", "delete"}}, @@ -78,6 +79,7 @@ func CommandCategories() []CommandCategory { {Name: "checkins", Category: "scheduling", Description: "View automatic check-ins", Actions: []string{"questions", "question", "answers", "answer"}}, {Name: "timeline", Category: "scheduling", Description: "View activity timelines", Actions: []string{}}, {Name: "reports", Category: "scheduling", Description: "View reports", Actions: []string{"assignable", "assigned", "overdue", "schedule"}}, + {Name: "assignments", Category: "scheduling", Description: "View my assignments", Actions: []string{"list", "completed", "due"}}, }, }, { @@ -99,6 +101,7 @@ func CommandCategories() []CommandCategory { {Name: "comments", Category: "communication", Description: "Manage comments", Actions: []string{"create", "list", "show", "update", "trash", "archive", "restore"}}, {Name: "attachments", Category: "communication", Description: "List attachments on items", Actions: []string{"list"}}, {Name: "boost", Category: "communication", Description: "Manage boosts (reactions)", Actions: []string{"list", "show", "create", "delete"}}, + {Name: "notifications", Category: "communication", Description: "View and manage notifications", Actions: []string{"list", "read"}}, }, }, { @@ -114,7 +117,7 @@ func CommandCategories() []CommandCategory { { Name: "Auth & Config", Commands: []CommandInfo{ - {Name: "accounts", Category: "auth", Description: "Manage accounts", Actions: []string{"list", "use"}}, + {Name: "accounts", Category: "auth", Description: "Manage accounts", Actions: []string{"list", "use", "show", "update", "logo"}}, {Name: "auth", Category: "auth", Description: "Authenticate with Basecamp", Actions: []string{"login", "logout", "status", "refresh"}}, {Name: "login", Category: "auth", Description: "Authenticate with Basecamp"}, {Name: "logout", Category: "auth", Description: "Remove stored credentials"}, diff --git a/internal/commands/commands_test.go b/internal/commands/commands_test.go index cb10c281b..70bd2933c 100644 --- a/internal/commands/commands_test.go +++ b/internal/commands/commands_test.go @@ -116,6 +116,9 @@ func buildRootWithAllCommands() *cobra.Command { root.AddCommand(commands.NewAttachCmd()) root.AddCommand(commands.NewUploadCmd()) root.AddCommand(commands.NewSkillCmd()) + root.AddCommand(commands.NewGaugesCmd()) + root.AddCommand(commands.NewAssignmentsCmd()) + root.AddCommand(commands.NewNotificationsCmd()) root.AddCommand(commands.NewTUICmd()) root.AddCommand(commands.NewProfileCmd()) root.AddCommand(commands.NewBonfireCmd()) diff --git a/internal/commands/notifications.go b/internal/commands/notifications.go new file mode 100644 index 000000000..d3dbfa2e7 --- /dev/null +++ b/internal/commands/notifications.go @@ -0,0 +1,195 @@ +package commands + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/basecamp/basecamp-cli/internal/appctx" + "github.com/basecamp/basecamp-cli/internal/output" +) + +// NewNotificationsCmd creates the notifications command. +func NewNotificationsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "notifications", + Short: "View and manage notifications", + Long: `View and manage your notifications. + +Shows unread, read, and memory notifications. Use 'read' to mark +notifications as read.`, + Annotations: map[string]string{ + "agent_notes": "Account-wide notifications — no --in needed.\n" + + "Returns unreads, reads, and memories sections.\n" + + "Use 'read' with notification IDs to mark as read.", + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runNotificationsList(cmd, 0) + }, + } + + cmd.AddCommand( + newNotificationsListCmd(), + newNotificationsReadCmd(), + ) + + return cmd +} + +func newNotificationsListCmd() *cobra.Command { + var page int32 + + cmd := &cobra.Command{ + Use: "list", + Short: "List notifications", + Long: "List notifications (same as bare 'notifications').", + RunE: func(cmd *cobra.Command, args []string) error { + return runNotificationsList(cmd, page) + }, + } + + cmd.Flags().Int32Var(&page, "page", 0, "Page number (default: first page)") + + return cmd +} + +func runNotificationsList(cmd *cobra.Command, page int32) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + result, err := app.Account().MyNotifications().Get(cmd.Context(), page) + if err != nil { + return convertSDKError(err) + } + + total := len(result.Unreads) + len(result.Reads) + len(result.Memories) + summary := fmt.Sprintf("%d notification(s)", total) + if len(result.Unreads) > 0 { + summary += fmt.Sprintf(" (%d unread)", len(result.Unreads)) + } + + nextPage := page + 1 + if page == 0 { + nextPage = 2 + } + breadcrumbs := []output.Breadcrumb{ + { + Action: "read", + Cmd: "basecamp notifications read ", + Description: "Mark as read", + }, + { + Action: "next", + Cmd: fmt.Sprintf("basecamp notifications list --page %d", nextPage), + Description: "Next page", + }, + } + + return app.OK(result, + output.WithSummary(summary), + output.WithBreadcrumbs(breadcrumbs...), + ) +} + +func newNotificationsReadCmd() *cobra.Command { + var page int32 + + cmd := &cobra.Command{ + Use: "read ...", + Short: "Mark notifications as read", + Long: `Mark one or more notifications as read. + +Accepts notification IDs from the page you were viewing. Use --page to +match the page you listed (defaults to first page). + + basecamp notifications read 12345 + basecamp notifications read 12345 67890 --page 2`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + // Parse the requested notification IDs + for _, arg := range args { + if _, err := strconv.ParseInt(arg, 10, 64); err != nil { + return output.ErrUsage(fmt.Sprintf("Invalid notification ID: %s", arg)) + } + } + + // Fetch the same page the user was looking at + result, err := app.Account().MyNotifications().Get(cmd.Context(), page) + if err != nil { + return convertSDKError(err) + } + + // Build ID → SGID map from all notification sections + sgidMap := make(map[int64]string) + for _, n := range result.Unreads { + if n.ReadableSGID != "" { + sgidMap[n.ID] = n.ReadableSGID + } + } + for _, n := range result.Reads { + if n.ReadableSGID != "" { + sgidMap[n.ID] = n.ReadableSGID + } + } + for _, n := range result.Memories { + if n.ReadableSGID != "" { + sgidMap[n.ID] = n.ReadableSGID + } + } + + // Resolve each requested ID to its SGID + var sgids []string + var unresolved []string + for _, arg := range args { + id, _ := strconv.ParseInt(arg, 10, 64) + if sgid, ok := sgidMap[id]; ok { + sgids = append(sgids, sgid) + } else { + unresolved = append(unresolved, arg) + } + } + + if len(unresolved) > 0 { + pageHint := "" + if page > 0 { + pageHint = fmt.Sprintf(" (page %d)", page) + } + return output.ErrUsageHint( + fmt.Sprintf("Notification(s) not found%s: %s", pageHint, strings.Join(unresolved, ", ")), + "Run 'basecamp notifications list' to see available notification IDs, then use --page to match", + ) + } + + err = app.Account().MyNotifications().MarkAsRead(cmd.Context(), sgids) + if err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"marked_read": len(sgids)}, + output.WithSummary(fmt.Sprintf("Marked %d notification(s) as read", len(sgids))), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "list", + Cmd: "basecamp notifications", + Description: "View notifications", + }, + ), + ) + }, + } + + cmd.Flags().Int32Var(&page, "page", 0, "Page to resolve IDs from (match the page you listed)") + + return cmd +} diff --git a/scripts/check-bare-groups.sh b/scripts/check-bare-groups.sh index 6c1b5a845..101aba7e9 100755 --- a/scripts/check-bare-groups.sh +++ b/scripts/check-bare-groups.sh @@ -15,8 +15,10 @@ ALLOWLIST=( NewSearchCmd # shortcut: performs search NewSetupCmd # wizard entry point NewSkillCmd # renders skill content - NewTimesheetCmd # shortcut: shows report - NewURLCmd # shortcut: opens URL + NewTimesheetCmd # shortcut: shows report + NewURLCmd # shortcut: opens URL + NewAssignmentsCmd # shortcut: shows assignments + NewNotificationsCmd # shortcut: lists notifications ) is_allowed() { From 6c0bca369c6bc609f343733ce38afb8d18c017e4 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 25 Mar 2026 00:30:48 -0700 Subject: [PATCH 6/6] Update docs and surface for v0.7.1 commands API-COVERAGE.md: 42 sections, 155 endpoints, SDK v0.7.1. SKILL.md: gauges, assignments, notifications, accounts sections. .surface: 148 commands, 422 flags. --- .surface | 615 +++++++++++++++++++++++++++++++++++++++ API-COVERAGE.md | 13 +- skills/basecamp/SKILL.md | 88 +++++- 3 files changed, 707 insertions(+), 9 deletions(-) diff --git a/.surface b/.surface index 385f64df4..70225d1ed 100644 --- a/.surface +++ b/.surface @@ -1,10 +1,13 @@ +ARG basecamp account logo upload 00 ARG basecamp account use 00 +ARG basecamp accounts logo upload 00 ARG basecamp accounts use 00 ARG basecamp api delete 00 ARG basecamp api get 00 ARG basecamp api post 00 ARG basecamp api put 00 ARG basecamp assign 00 ... +ARG basecamp assignments due 00 [scope] ARG basecamp attach 00 ARG basecamp attach 01 [ @@ -200,6 +203,9 @@ ARG basecamp forwards replies 00 ARG basecamp forwards reply 00 ARG basecamp forwards reply 01 ARG basecamp forwards show 00 +ARG basecamp gauges delete 00 +ARG basecamp gauges needle 00 +ARG basecamp gauges update 00 ARG basecamp help 00 [command] ARG basecamp hillcharts track 00 ARG basecamp hillcharts untrack 00 @@ -236,6 +242,7 @@ ARG basecamp msgs show 00 ARG basecamp msgs trash 00 ARG basecamp msgs unpin 00 ARG basecamp msgs update 00 +ARG basecamp notifications read 00 ... ARG basecamp people add 00 ... ARG basecamp people remove 00 ... ARG basecamp people show 00 @@ -414,9 +421,19 @@ ARG basecamp webhooks update 00 CMD basecamp CMD basecamp account CMD basecamp account list +CMD basecamp account logo +CMD basecamp account logo remove +CMD basecamp account logo upload +CMD basecamp account show +CMD basecamp account update CMD basecamp account use CMD basecamp accounts CMD basecamp accounts list +CMD basecamp accounts logo +CMD basecamp accounts logo remove +CMD basecamp accounts logo upload +CMD basecamp accounts show +CMD basecamp accounts update CMD basecamp accounts use CMD basecamp api CMD basecamp api delete @@ -424,6 +441,10 @@ CMD basecamp api get CMD basecamp api post CMD basecamp api put CMD basecamp assign +CMD basecamp assignments +CMD basecamp assignments completed +CMD basecamp assignments due +CMD basecamp assignments list CMD basecamp attach CMD basecamp attachments CMD basecamp attachments list @@ -730,6 +751,15 @@ CMD basecamp forwards list CMD basecamp forwards replies CMD basecamp forwards reply CMD basecamp forwards show +CMD basecamp gauges +CMD basecamp gauges create +CMD basecamp gauges delete +CMD basecamp gauges disable +CMD basecamp gauges enable +CMD basecamp gauges list +CMD basecamp gauges needle +CMD basecamp gauges needles +CMD basecamp gauges update CMD basecamp help CMD basecamp hillcharts CMD basecamp hillcharts show @@ -775,6 +805,9 @@ CMD basecamp msgs show CMD basecamp msgs trash CMD basecamp msgs unpin CMD basecamp msgs update +CMD basecamp notifications +CMD basecamp notifications list +CMD basecamp notifications read CMD basecamp people CMD basecamp people add CMD basecamp people list @@ -1088,6 +1121,112 @@ FLAG basecamp account list --stats type=bool FLAG basecamp account list --styled type=bool FLAG basecamp account list --todolist type=string FLAG basecamp account list --verbose type=count +FLAG basecamp account logo --account type=string +FLAG basecamp account logo --agent type=bool +FLAG basecamp account logo --cache-dir type=string +FLAG basecamp account logo --count type=bool +FLAG basecamp account logo --help type=bool +FLAG basecamp account logo --hints type=bool +FLAG basecamp account logo --ids-only type=bool +FLAG basecamp account logo --in type=string +FLAG basecamp account logo --jq type=string +FLAG basecamp account logo --json type=bool +FLAG basecamp account logo --markdown type=bool +FLAG basecamp account logo --md type=bool +FLAG basecamp account logo --no-hints type=bool +FLAG basecamp account logo --no-stats type=bool +FLAG basecamp account logo --profile type=string +FLAG basecamp account logo --project type=string +FLAG basecamp account logo --quiet type=bool +FLAG basecamp account logo --stats type=bool +FLAG basecamp account logo --styled type=bool +FLAG basecamp account logo --todolist type=string +FLAG basecamp account logo --verbose type=count +FLAG basecamp account logo remove --account type=string +FLAG basecamp account logo remove --agent type=bool +FLAG basecamp account logo remove --cache-dir type=string +FLAG basecamp account logo remove --count type=bool +FLAG basecamp account logo remove --help type=bool +FLAG basecamp account logo remove --hints type=bool +FLAG basecamp account logo remove --ids-only type=bool +FLAG basecamp account logo remove --in type=string +FLAG basecamp account logo remove --jq type=string +FLAG basecamp account logo remove --json type=bool +FLAG basecamp account logo remove --markdown type=bool +FLAG basecamp account logo remove --md type=bool +FLAG basecamp account logo remove --no-hints type=bool +FLAG basecamp account logo remove --no-stats type=bool +FLAG basecamp account logo remove --profile type=string +FLAG basecamp account logo remove --project type=string +FLAG basecamp account logo remove --quiet type=bool +FLAG basecamp account logo remove --stats type=bool +FLAG basecamp account logo remove --styled type=bool +FLAG basecamp account logo remove --todolist type=string +FLAG basecamp account logo remove --verbose type=count +FLAG basecamp account logo upload --account type=string +FLAG basecamp account logo upload --agent type=bool +FLAG basecamp account logo upload --cache-dir type=string +FLAG basecamp account logo upload --count type=bool +FLAG basecamp account logo upload --help type=bool +FLAG basecamp account logo upload --hints type=bool +FLAG basecamp account logo upload --ids-only type=bool +FLAG basecamp account logo upload --in type=string +FLAG basecamp account logo upload --jq type=string +FLAG basecamp account logo upload --json type=bool +FLAG basecamp account logo upload --markdown type=bool +FLAG basecamp account logo upload --md type=bool +FLAG basecamp account logo upload --no-hints type=bool +FLAG basecamp account logo upload --no-stats type=bool +FLAG basecamp account logo upload --profile type=string +FLAG basecamp account logo upload --project type=string +FLAG basecamp account logo upload --quiet type=bool +FLAG basecamp account logo upload --stats type=bool +FLAG basecamp account logo upload --styled type=bool +FLAG basecamp account logo upload --todolist type=string +FLAG basecamp account logo upload --verbose type=count +FLAG basecamp account show --account type=string +FLAG basecamp account show --agent type=bool +FLAG basecamp account show --cache-dir type=string +FLAG basecamp account show --count type=bool +FLAG basecamp account show --help type=bool +FLAG basecamp account show --hints type=bool +FLAG basecamp account show --ids-only type=bool +FLAG basecamp account show --in type=string +FLAG basecamp account show --jq type=string +FLAG basecamp account show --json type=bool +FLAG basecamp account show --markdown type=bool +FLAG basecamp account show --md type=bool +FLAG basecamp account show --no-hints type=bool +FLAG basecamp account show --no-stats type=bool +FLAG basecamp account show --profile type=string +FLAG basecamp account show --project type=string +FLAG basecamp account show --quiet type=bool +FLAG basecamp account show --stats type=bool +FLAG basecamp account show --styled type=bool +FLAG basecamp account show --todolist type=string +FLAG basecamp account show --verbose type=count +FLAG basecamp account update --account type=string +FLAG basecamp account update --agent type=bool +FLAG basecamp account update --cache-dir type=string +FLAG basecamp account update --count type=bool +FLAG basecamp account update --help type=bool +FLAG basecamp account update --hints type=bool +FLAG basecamp account update --ids-only type=bool +FLAG basecamp account update --in type=string +FLAG basecamp account update --jq type=string +FLAG basecamp account update --json type=bool +FLAG basecamp account update --markdown type=bool +FLAG basecamp account update --md type=bool +FLAG basecamp account update --name type=string +FLAG basecamp account update --no-hints type=bool +FLAG basecamp account update --no-stats type=bool +FLAG basecamp account update --profile type=string +FLAG basecamp account update --project type=string +FLAG basecamp account update --quiet type=bool +FLAG basecamp account update --stats type=bool +FLAG basecamp account update --styled type=bool +FLAG basecamp account update --todolist type=string +FLAG basecamp account update --verbose type=count FLAG basecamp account use --account type=string FLAG basecamp account use --agent type=bool FLAG basecamp account use --cache-dir type=string @@ -1152,6 +1291,112 @@ FLAG basecamp accounts list --stats type=bool FLAG basecamp accounts list --styled type=bool FLAG basecamp accounts list --todolist type=string FLAG basecamp accounts list --verbose type=count +FLAG basecamp accounts logo --account type=string +FLAG basecamp accounts logo --agent type=bool +FLAG basecamp accounts logo --cache-dir type=string +FLAG basecamp accounts logo --count type=bool +FLAG basecamp accounts logo --help type=bool +FLAG basecamp accounts logo --hints type=bool +FLAG basecamp accounts logo --ids-only type=bool +FLAG basecamp accounts logo --in type=string +FLAG basecamp accounts logo --jq type=string +FLAG basecamp accounts logo --json type=bool +FLAG basecamp accounts logo --markdown type=bool +FLAG basecamp accounts logo --md type=bool +FLAG basecamp accounts logo --no-hints type=bool +FLAG basecamp accounts logo --no-stats type=bool +FLAG basecamp accounts logo --profile type=string +FLAG basecamp accounts logo --project type=string +FLAG basecamp accounts logo --quiet type=bool +FLAG basecamp accounts logo --stats type=bool +FLAG basecamp accounts logo --styled type=bool +FLAG basecamp accounts logo --todolist type=string +FLAG basecamp accounts logo --verbose type=count +FLAG basecamp accounts logo remove --account type=string +FLAG basecamp accounts logo remove --agent type=bool +FLAG basecamp accounts logo remove --cache-dir type=string +FLAG basecamp accounts logo remove --count type=bool +FLAG basecamp accounts logo remove --help type=bool +FLAG basecamp accounts logo remove --hints type=bool +FLAG basecamp accounts logo remove --ids-only type=bool +FLAG basecamp accounts logo remove --in type=string +FLAG basecamp accounts logo remove --jq type=string +FLAG basecamp accounts logo remove --json type=bool +FLAG basecamp accounts logo remove --markdown type=bool +FLAG basecamp accounts logo remove --md type=bool +FLAG basecamp accounts logo remove --no-hints type=bool +FLAG basecamp accounts logo remove --no-stats type=bool +FLAG basecamp accounts logo remove --profile type=string +FLAG basecamp accounts logo remove --project type=string +FLAG basecamp accounts logo remove --quiet type=bool +FLAG basecamp accounts logo remove --stats type=bool +FLAG basecamp accounts logo remove --styled type=bool +FLAG basecamp accounts logo remove --todolist type=string +FLAG basecamp accounts logo remove --verbose type=count +FLAG basecamp accounts logo upload --account type=string +FLAG basecamp accounts logo upload --agent type=bool +FLAG basecamp accounts logo upload --cache-dir type=string +FLAG basecamp accounts logo upload --count type=bool +FLAG basecamp accounts logo upload --help type=bool +FLAG basecamp accounts logo upload --hints type=bool +FLAG basecamp accounts logo upload --ids-only type=bool +FLAG basecamp accounts logo upload --in type=string +FLAG basecamp accounts logo upload --jq type=string +FLAG basecamp accounts logo upload --json type=bool +FLAG basecamp accounts logo upload --markdown type=bool +FLAG basecamp accounts logo upload --md type=bool +FLAG basecamp accounts logo upload --no-hints type=bool +FLAG basecamp accounts logo upload --no-stats type=bool +FLAG basecamp accounts logo upload --profile type=string +FLAG basecamp accounts logo upload --project type=string +FLAG basecamp accounts logo upload --quiet type=bool +FLAG basecamp accounts logo upload --stats type=bool +FLAG basecamp accounts logo upload --styled type=bool +FLAG basecamp accounts logo upload --todolist type=string +FLAG basecamp accounts logo upload --verbose type=count +FLAG basecamp accounts show --account type=string +FLAG basecamp accounts show --agent type=bool +FLAG basecamp accounts show --cache-dir type=string +FLAG basecamp accounts show --count type=bool +FLAG basecamp accounts show --help type=bool +FLAG basecamp accounts show --hints type=bool +FLAG basecamp accounts show --ids-only type=bool +FLAG basecamp accounts show --in type=string +FLAG basecamp accounts show --jq type=string +FLAG basecamp accounts show --json type=bool +FLAG basecamp accounts show --markdown type=bool +FLAG basecamp accounts show --md type=bool +FLAG basecamp accounts show --no-hints type=bool +FLAG basecamp accounts show --no-stats type=bool +FLAG basecamp accounts show --profile type=string +FLAG basecamp accounts show --project type=string +FLAG basecamp accounts show --quiet type=bool +FLAG basecamp accounts show --stats type=bool +FLAG basecamp accounts show --styled type=bool +FLAG basecamp accounts show --todolist type=string +FLAG basecamp accounts show --verbose type=count +FLAG basecamp accounts update --account type=string +FLAG basecamp accounts update --agent type=bool +FLAG basecamp accounts update --cache-dir type=string +FLAG basecamp accounts update --count type=bool +FLAG basecamp accounts update --help type=bool +FLAG basecamp accounts update --hints type=bool +FLAG basecamp accounts update --ids-only type=bool +FLAG basecamp accounts update --in type=string +FLAG basecamp accounts update --jq type=string +FLAG basecamp accounts update --json type=bool +FLAG basecamp accounts update --markdown type=bool +FLAG basecamp accounts update --md type=bool +FLAG basecamp accounts update --name type=string +FLAG basecamp accounts update --no-hints type=bool +FLAG basecamp accounts update --no-stats type=bool +FLAG basecamp accounts update --profile type=string +FLAG basecamp accounts update --project type=string +FLAG basecamp accounts update --quiet type=bool +FLAG basecamp accounts update --stats type=bool +FLAG basecamp accounts update --styled type=bool +FLAG basecamp accounts update --todolist type=string +FLAG basecamp accounts update --verbose type=count FLAG basecamp accounts use --account type=string FLAG basecamp accounts use --agent type=bool FLAG basecamp accounts use --cache-dir type=string @@ -1305,6 +1550,90 @@ FLAG basecamp assign --styled type=bool FLAG basecamp assign --to type=string FLAG basecamp assign --todolist type=string FLAG basecamp assign --verbose type=count +FLAG basecamp assignments --account type=string +FLAG basecamp assignments --agent type=bool +FLAG basecamp assignments --cache-dir type=string +FLAG basecamp assignments --count type=bool +FLAG basecamp assignments --help type=bool +FLAG basecamp assignments --hints type=bool +FLAG basecamp assignments --ids-only type=bool +FLAG basecamp assignments --in type=string +FLAG basecamp assignments --jq type=string +FLAG basecamp assignments --json type=bool +FLAG basecamp assignments --markdown type=bool +FLAG basecamp assignments --md type=bool +FLAG basecamp assignments --no-hints type=bool +FLAG basecamp assignments --no-stats type=bool +FLAG basecamp assignments --profile type=string +FLAG basecamp assignments --project type=string +FLAG basecamp assignments --quiet type=bool +FLAG basecamp assignments --stats type=bool +FLAG basecamp assignments --styled type=bool +FLAG basecamp assignments --todolist type=string +FLAG basecamp assignments --verbose type=count +FLAG basecamp assignments completed --account type=string +FLAG basecamp assignments completed --agent type=bool +FLAG basecamp assignments completed --cache-dir type=string +FLAG basecamp assignments completed --count type=bool +FLAG basecamp assignments completed --help type=bool +FLAG basecamp assignments completed --hints type=bool +FLAG basecamp assignments completed --ids-only type=bool +FLAG basecamp assignments completed --in type=string +FLAG basecamp assignments completed --jq type=string +FLAG basecamp assignments completed --json type=bool +FLAG basecamp assignments completed --markdown type=bool +FLAG basecamp assignments completed --md type=bool +FLAG basecamp assignments completed --no-hints type=bool +FLAG basecamp assignments completed --no-stats type=bool +FLAG basecamp assignments completed --profile type=string +FLAG basecamp assignments completed --project type=string +FLAG basecamp assignments completed --quiet type=bool +FLAG basecamp assignments completed --stats type=bool +FLAG basecamp assignments completed --styled type=bool +FLAG basecamp assignments completed --todolist type=string +FLAG basecamp assignments completed --verbose type=count +FLAG basecamp assignments due --account type=string +FLAG basecamp assignments due --agent type=bool +FLAG basecamp assignments due --cache-dir type=string +FLAG basecamp assignments due --count type=bool +FLAG basecamp assignments due --help type=bool +FLAG basecamp assignments due --hints type=bool +FLAG basecamp assignments due --ids-only type=bool +FLAG basecamp assignments due --in type=string +FLAG basecamp assignments due --jq type=string +FLAG basecamp assignments due --json type=bool +FLAG basecamp assignments due --markdown type=bool +FLAG basecamp assignments due --md type=bool +FLAG basecamp assignments due --no-hints type=bool +FLAG basecamp assignments due --no-stats type=bool +FLAG basecamp assignments due --profile type=string +FLAG basecamp assignments due --project type=string +FLAG basecamp assignments due --quiet type=bool +FLAG basecamp assignments due --stats type=bool +FLAG basecamp assignments due --styled type=bool +FLAG basecamp assignments due --todolist type=string +FLAG basecamp assignments due --verbose type=count +FLAG basecamp assignments list --account type=string +FLAG basecamp assignments list --agent type=bool +FLAG basecamp assignments list --cache-dir type=string +FLAG basecamp assignments list --count type=bool +FLAG basecamp assignments list --help type=bool +FLAG basecamp assignments list --hints type=bool +FLAG basecamp assignments list --ids-only type=bool +FLAG basecamp assignments list --in type=string +FLAG basecamp assignments list --jq type=string +FLAG basecamp assignments list --json type=bool +FLAG basecamp assignments list --markdown type=bool +FLAG basecamp assignments list --md type=bool +FLAG basecamp assignments list --no-hints type=bool +FLAG basecamp assignments list --no-stats type=bool +FLAG basecamp assignments list --profile type=string +FLAG basecamp assignments list --project type=string +FLAG basecamp assignments list --quiet type=bool +FLAG basecamp assignments list --stats type=bool +FLAG basecamp assignments list --styled type=bool +FLAG basecamp assignments list --todolist type=string +FLAG basecamp assignments list --verbose type=count FLAG basecamp attach --account type=string FLAG basecamp attach --agent type=bool FLAG basecamp attach --cache-dir type=string @@ -8648,6 +8977,201 @@ FLAG basecamp forwards show --stats type=bool FLAG basecamp forwards show --styled type=bool FLAG basecamp forwards show --todolist type=string FLAG basecamp forwards show --verbose type=count +FLAG basecamp gauges --account type=string +FLAG basecamp gauges --agent type=bool +FLAG basecamp gauges --cache-dir type=string +FLAG basecamp gauges --count type=bool +FLAG basecamp gauges --help type=bool +FLAG basecamp gauges --hints type=bool +FLAG basecamp gauges --ids-only type=bool +FLAG basecamp gauges --in type=string +FLAG basecamp gauges --jq type=string +FLAG basecamp gauges --json type=bool +FLAG basecamp gauges --markdown type=bool +FLAG basecamp gauges --md type=bool +FLAG basecamp gauges --no-hints type=bool +FLAG basecamp gauges --no-stats type=bool +FLAG basecamp gauges --profile type=string +FLAG basecamp gauges --project type=string +FLAG basecamp gauges --quiet type=bool +FLAG basecamp gauges --stats type=bool +FLAG basecamp gauges --styled type=bool +FLAG basecamp gauges --todolist type=string +FLAG basecamp gauges --verbose type=count +FLAG basecamp gauges create --account type=string +FLAG basecamp gauges create --agent type=bool +FLAG basecamp gauges create --cache-dir type=string +FLAG basecamp gauges create --color type=string +FLAG basecamp gauges create --count type=bool +FLAG basecamp gauges create --description type=string +FLAG basecamp gauges create --help type=bool +FLAG basecamp gauges create --hints type=bool +FLAG basecamp gauges create --ids-only type=bool +FLAG basecamp gauges create --in type=string +FLAG basecamp gauges create --jq type=string +FLAG basecamp gauges create --json type=bool +FLAG basecamp gauges create --markdown type=bool +FLAG basecamp gauges create --md type=bool +FLAG basecamp gauges create --no-hints type=bool +FLAG basecamp gauges create --no-stats type=bool +FLAG basecamp gauges create --notify type=string +FLAG basecamp gauges create --position type=int32 +FLAG basecamp gauges create --profile type=string +FLAG basecamp gauges create --project type=string +FLAG basecamp gauges create --quiet type=bool +FLAG basecamp gauges create --stats type=bool +FLAG basecamp gauges create --styled type=bool +FLAG basecamp gauges create --subscriptions type=int64Slice +FLAG basecamp gauges create --todolist type=string +FLAG basecamp gauges create --verbose type=count +FLAG basecamp gauges delete --account type=string +FLAG basecamp gauges delete --agent type=bool +FLAG basecamp gauges delete --cache-dir type=string +FLAG basecamp gauges delete --count type=bool +FLAG basecamp gauges delete --help type=bool +FLAG basecamp gauges delete --hints type=bool +FLAG basecamp gauges delete --ids-only type=bool +FLAG basecamp gauges delete --in type=string +FLAG basecamp gauges delete --jq type=string +FLAG basecamp gauges delete --json type=bool +FLAG basecamp gauges delete --markdown type=bool +FLAG basecamp gauges delete --md type=bool +FLAG basecamp gauges delete --no-hints type=bool +FLAG basecamp gauges delete --no-stats type=bool +FLAG basecamp gauges delete --profile type=string +FLAG basecamp gauges delete --project type=string +FLAG basecamp gauges delete --quiet type=bool +FLAG basecamp gauges delete --stats type=bool +FLAG basecamp gauges delete --styled type=bool +FLAG basecamp gauges delete --todolist type=string +FLAG basecamp gauges delete --verbose type=count +FLAG basecamp gauges disable --account type=string +FLAG basecamp gauges disable --agent type=bool +FLAG basecamp gauges disable --cache-dir type=string +FLAG basecamp gauges disable --count type=bool +FLAG basecamp gauges disable --help type=bool +FLAG basecamp gauges disable --hints type=bool +FLAG basecamp gauges disable --ids-only type=bool +FLAG basecamp gauges disable --in type=string +FLAG basecamp gauges disable --jq type=string +FLAG basecamp gauges disable --json type=bool +FLAG basecamp gauges disable --markdown type=bool +FLAG basecamp gauges disable --md type=bool +FLAG basecamp gauges disable --no-hints type=bool +FLAG basecamp gauges disable --no-stats type=bool +FLAG basecamp gauges disable --profile type=string +FLAG basecamp gauges disable --project type=string +FLAG basecamp gauges disable --quiet type=bool +FLAG basecamp gauges disable --stats type=bool +FLAG basecamp gauges disable --styled type=bool +FLAG basecamp gauges disable --todolist type=string +FLAG basecamp gauges disable --verbose type=count +FLAG basecamp gauges enable --account type=string +FLAG basecamp gauges enable --agent type=bool +FLAG basecamp gauges enable --cache-dir type=string +FLAG basecamp gauges enable --count type=bool +FLAG basecamp gauges enable --help type=bool +FLAG basecamp gauges enable --hints type=bool +FLAG basecamp gauges enable --ids-only type=bool +FLAG basecamp gauges enable --in type=string +FLAG basecamp gauges enable --jq type=string +FLAG basecamp gauges enable --json type=bool +FLAG basecamp gauges enable --markdown type=bool +FLAG basecamp gauges enable --md type=bool +FLAG basecamp gauges enable --no-hints type=bool +FLAG basecamp gauges enable --no-stats type=bool +FLAG basecamp gauges enable --profile type=string +FLAG basecamp gauges enable --project type=string +FLAG basecamp gauges enable --quiet type=bool +FLAG basecamp gauges enable --stats type=bool +FLAG basecamp gauges enable --styled type=bool +FLAG basecamp gauges enable --todolist type=string +FLAG basecamp gauges enable --verbose type=count +FLAG basecamp gauges list --account type=string +FLAG basecamp gauges list --agent type=bool +FLAG basecamp gauges list --cache-dir type=string +FLAG basecamp gauges list --count type=bool +FLAG basecamp gauges list --help type=bool +FLAG basecamp gauges list --hints type=bool +FLAG basecamp gauges list --ids-only type=bool +FLAG basecamp gauges list --in type=string +FLAG basecamp gauges list --jq type=string +FLAG basecamp gauges list --json type=bool +FLAG basecamp gauges list --markdown type=bool +FLAG basecamp gauges list --md type=bool +FLAG basecamp gauges list --no-hints type=bool +FLAG basecamp gauges list --no-stats type=bool +FLAG basecamp gauges list --profile type=string +FLAG basecamp gauges list --project type=string +FLAG basecamp gauges list --quiet type=bool +FLAG basecamp gauges list --stats type=bool +FLAG basecamp gauges list --styled type=bool +FLAG basecamp gauges list --todolist type=string +FLAG basecamp gauges list --verbose type=count +FLAG basecamp gauges needle --account type=string +FLAG basecamp gauges needle --agent type=bool +FLAG basecamp gauges needle --cache-dir type=string +FLAG basecamp gauges needle --count type=bool +FLAG basecamp gauges needle --help type=bool +FLAG basecamp gauges needle --hints type=bool +FLAG basecamp gauges needle --ids-only type=bool +FLAG basecamp gauges needle --in type=string +FLAG basecamp gauges needle --jq type=string +FLAG basecamp gauges needle --json type=bool +FLAG basecamp gauges needle --markdown type=bool +FLAG basecamp gauges needle --md type=bool +FLAG basecamp gauges needle --no-hints type=bool +FLAG basecamp gauges needle --no-stats type=bool +FLAG basecamp gauges needle --profile type=string +FLAG basecamp gauges needle --project type=string +FLAG basecamp gauges needle --quiet type=bool +FLAG basecamp gauges needle --stats type=bool +FLAG basecamp gauges needle --styled type=bool +FLAG basecamp gauges needle --todolist type=string +FLAG basecamp gauges needle --verbose type=count +FLAG basecamp gauges needles --account type=string +FLAG basecamp gauges needles --agent type=bool +FLAG basecamp gauges needles --cache-dir type=string +FLAG basecamp gauges needles --count type=bool +FLAG basecamp gauges needles --help type=bool +FLAG basecamp gauges needles --hints type=bool +FLAG basecamp gauges needles --ids-only type=bool +FLAG basecamp gauges needles --in type=string +FLAG basecamp gauges needles --jq type=string +FLAG basecamp gauges needles --json type=bool +FLAG basecamp gauges needles --markdown type=bool +FLAG basecamp gauges needles --md type=bool +FLAG basecamp gauges needles --no-hints type=bool +FLAG basecamp gauges needles --no-stats type=bool +FLAG basecamp gauges needles --profile type=string +FLAG basecamp gauges needles --project type=string +FLAG basecamp gauges needles --quiet type=bool +FLAG basecamp gauges needles --stats type=bool +FLAG basecamp gauges needles --styled type=bool +FLAG basecamp gauges needles --todolist type=string +FLAG basecamp gauges needles --verbose type=count +FLAG basecamp gauges update --account type=string +FLAG basecamp gauges update --agent type=bool +FLAG basecamp gauges update --cache-dir type=string +FLAG basecamp gauges update --count type=bool +FLAG basecamp gauges update --description type=string +FLAG basecamp gauges update --help type=bool +FLAG basecamp gauges update --hints type=bool +FLAG basecamp gauges update --ids-only type=bool +FLAG basecamp gauges update --in type=string +FLAG basecamp gauges update --jq type=string +FLAG basecamp gauges update --json type=bool +FLAG basecamp gauges update --markdown type=bool +FLAG basecamp gauges update --md type=bool +FLAG basecamp gauges update --no-hints type=bool +FLAG basecamp gauges update --no-stats type=bool +FLAG basecamp gauges update --profile type=string +FLAG basecamp gauges update --project type=string +FLAG basecamp gauges update --quiet type=bool +FLAG basecamp gauges update --stats type=bool +FLAG basecamp gauges update --styled type=bool +FLAG basecamp gauges update --todolist type=string +FLAG basecamp gauges update --verbose type=count FLAG basecamp help --account type=string FLAG basecamp help --agent type=bool FLAG basecamp help --cache-dir type=string @@ -9660,6 +10184,71 @@ FLAG basecamp msgs update --styled type=bool FLAG basecamp msgs update --title type=string FLAG basecamp msgs update --todolist type=string FLAG basecamp msgs update --verbose type=count +FLAG basecamp notifications --account type=string +FLAG basecamp notifications --agent type=bool +FLAG basecamp notifications --cache-dir type=string +FLAG basecamp notifications --count type=bool +FLAG basecamp notifications --help type=bool +FLAG basecamp notifications --hints type=bool +FLAG basecamp notifications --ids-only type=bool +FLAG basecamp notifications --in type=string +FLAG basecamp notifications --jq type=string +FLAG basecamp notifications --json type=bool +FLAG basecamp notifications --markdown type=bool +FLAG basecamp notifications --md type=bool +FLAG basecamp notifications --no-hints type=bool +FLAG basecamp notifications --no-stats type=bool +FLAG basecamp notifications --profile type=string +FLAG basecamp notifications --project type=string +FLAG basecamp notifications --quiet type=bool +FLAG basecamp notifications --stats type=bool +FLAG basecamp notifications --styled type=bool +FLAG basecamp notifications --todolist type=string +FLAG basecamp notifications --verbose type=count +FLAG basecamp notifications list --account type=string +FLAG basecamp notifications list --agent type=bool +FLAG basecamp notifications list --cache-dir type=string +FLAG basecamp notifications list --count type=bool +FLAG basecamp notifications list --help type=bool +FLAG basecamp notifications list --hints type=bool +FLAG basecamp notifications list --ids-only type=bool +FLAG basecamp notifications list --in type=string +FLAG basecamp notifications list --jq type=string +FLAG basecamp notifications list --json type=bool +FLAG basecamp notifications list --markdown type=bool +FLAG basecamp notifications list --md type=bool +FLAG basecamp notifications list --no-hints type=bool +FLAG basecamp notifications list --no-stats type=bool +FLAG basecamp notifications list --page type=int32 +FLAG basecamp notifications list --profile type=string +FLAG basecamp notifications list --project type=string +FLAG basecamp notifications list --quiet type=bool +FLAG basecamp notifications list --stats type=bool +FLAG basecamp notifications list --styled type=bool +FLAG basecamp notifications list --todolist type=string +FLAG basecamp notifications list --verbose type=count +FLAG basecamp notifications read --account type=string +FLAG basecamp notifications read --agent type=bool +FLAG basecamp notifications read --cache-dir type=string +FLAG basecamp notifications read --count type=bool +FLAG basecamp notifications read --help type=bool +FLAG basecamp notifications read --hints type=bool +FLAG basecamp notifications read --ids-only type=bool +FLAG basecamp notifications read --in type=string +FLAG basecamp notifications read --jq type=string +FLAG basecamp notifications read --json type=bool +FLAG basecamp notifications read --markdown type=bool +FLAG basecamp notifications read --md type=bool +FLAG basecamp notifications read --no-hints type=bool +FLAG basecamp notifications read --no-stats type=bool +FLAG basecamp notifications read --page type=int32 +FLAG basecamp notifications read --profile type=string +FLAG basecamp notifications read --project type=string +FLAG basecamp notifications read --quiet type=bool +FLAG basecamp notifications read --stats type=bool +FLAG basecamp notifications read --styled type=bool +FLAG basecamp notifications read --todolist type=string +FLAG basecamp notifications read --verbose type=count FLAG basecamp people --account type=string FLAG basecamp people --agent type=bool FLAG basecamp people --cache-dir type=string @@ -15487,9 +16076,19 @@ FLAG basecamp webhooks update --url type=string FLAG basecamp webhooks update --verbose type=count SUB basecamp account SUB basecamp account list +SUB basecamp account logo +SUB basecamp account logo remove +SUB basecamp account logo upload +SUB basecamp account show +SUB basecamp account update SUB basecamp account use SUB basecamp accounts SUB basecamp accounts list +SUB basecamp accounts logo +SUB basecamp accounts logo remove +SUB basecamp accounts logo upload +SUB basecamp accounts show +SUB basecamp accounts update SUB basecamp accounts use SUB basecamp api SUB basecamp api delete @@ -15497,6 +16096,10 @@ SUB basecamp api get SUB basecamp api post SUB basecamp api put SUB basecamp assign +SUB basecamp assignments +SUB basecamp assignments completed +SUB basecamp assignments due +SUB basecamp assignments list SUB basecamp attach SUB basecamp attachments SUB basecamp attachments list @@ -15803,6 +16406,15 @@ SUB basecamp forwards list SUB basecamp forwards replies SUB basecamp forwards reply SUB basecamp forwards show +SUB basecamp gauges +SUB basecamp gauges create +SUB basecamp gauges delete +SUB basecamp gauges disable +SUB basecamp gauges enable +SUB basecamp gauges list +SUB basecamp gauges needle +SUB basecamp gauges needles +SUB basecamp gauges update SUB basecamp help SUB basecamp hillcharts SUB basecamp hillcharts show @@ -15848,6 +16460,9 @@ SUB basecamp msgs show SUB basecamp msgs trash SUB basecamp msgs unpin SUB basecamp msgs update +SUB basecamp notifications +SUB basecamp notifications list +SUB basecamp notifications read SUB basecamp people SUB basecamp people add SUB basecamp people list diff --git a/API-COVERAGE.md b/API-COVERAGE.md index bfc597d97..caa6add2a 100644 --- a/API-COVERAGE.md +++ b/API-COVERAGE.md @@ -6,15 +6,15 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github. | Status | Sections | Endpoints | |--------|----------|-----------| -| ✅ Implemented | 38 | 139 | +| ✅ Implemented | 42 | 155 | | ⏭️ Out of scope | 4 | 12 | -| **Total (docs)** | **42** | **151** | +| **Total (docs)** | **46** | **167** | -**100% coverage of in-scope API** (139/139 endpoints) +**100% coverage of in-scope API** (155/155 endpoints) Out-of-scope sections are excluded from parity totals and scripts: chatbots (different auth), legacy Clientside (deprecated) -**SDK version:** v0.6.0 — uniform pagination (Limit/Page) on all List methods; `types.FlexibleTime` and `types.FlexInt` for wire format handling. +**SDK version:** v0.7.1 — adds gauges, my assignments, my notifications, and account management services. Fixes Person.Id deserialization and Account.Logo type mismatch. ## Coverage by Section @@ -28,6 +28,7 @@ Out-of-scope sections are excluded from parity totals and scripts: chatbots (dif | todolist_groups | 8 | `todolistgroups` | ✅ | - | list, show, create, update, position | | **Hill Charts** | | hill_charts | 2 | `hillcharts` | ✅ | - | show, track/untrack todolists | +| gauges | 7 | `gauges` | ✅ | - | list, needles, needle, create, update, delete, enable/disable | | **Communication** | | messages | 10 | `messages`, `message` | ✅ | - | list, show, create, update, publish, pin, unpin. Create supports `--subscribe`/`--no-subscribe` and `--draft`. Publish promotes drafts to active | | message_boards | 3 | `messageboards` | ✅ | - | Container, accessed via project dock | @@ -35,6 +36,7 @@ Out-of-scope sections are excluded from parity totals and scripts: chatbots (dif | campfires | 14 | `chat` | ✅ | - | list, messages, post, line show/delete. @mentions in content | | comments | 8 | `comment`, `comments` | ✅ | - | list, show, create, update. @mentions in content | | boosts | 6 | `boost`, `react` | ✅ | - | list (recording + event), show, create (recording + event), delete | +| notifications | 2 | `notifications` | ✅ | - | list, mark as read | | **Cards (Kanban)** | | card_tables | 3 | `cards` | ✅ | - | Accessed via project dock | | card_table_cards | 9 | `cards` | ✅ | - | list, show, create, update, move | @@ -43,6 +45,7 @@ Out-of-scope sections are excluded from parity totals and scripts: chatbots (dif | **People** | | people | 12 | `people`, `me` | ✅ | - | list, show, pingable, add, remove | | **Search & Recordings** | +| my_assignments | 3 | `assignments` | ✅ | - | list (priorities/non-priorities), completed, due (with scope filter) | | search | 2 | `search` | ✅ | - | Full-text search | | recordings | 4 | `recordings` | ✅ | - | Browse by type/status, trash/archive/restore | | **Files & Documents** | @@ -78,6 +81,8 @@ Out-of-scope sections are excluded from parity totals and scripts: chatbots (dif | client_replies | 6 | - | ⏭️ | skip | Legacy Clientside only (see notes) | | **Chatbots** | | chatbots | 10 | - | ⏭️ | skip | Requires chatbot key, not OAuth (see notes) | +| **Account** | +| account | 4 | `accounts` | ✅ | - | show, update name, upload logo, remove logo | | **Lineup** | | lineup_markers | 4 | `lineup` | ✅ | - | list, create, update, delete markers | | **Reference Only** | diff --git a/skills/basecamp/SKILL.md b/skills/basecamp/SKILL.md index d4b131a52..ccee186c1 100644 --- a/skills/basecamp/SKILL.md +++ b/skills/basecamp/SKILL.md @@ -3,7 +3,8 @@ name: basecamp description: | Interact with Basecamp via the Basecamp CLI. Full API coverage: projects, todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, - subscriptions, lineup, and chat. Use for ANY Basecamp question or action. + subscriptions, lineup, chat, gauges, assignments, notifications, and accounts. + Use for ANY Basecamp question or action. triggers: # Direct invocations - basecamp @@ -23,6 +24,10 @@ triggers: - basecamp timeline - basecamp template - basecamp webhook + - basecamp gauge + - basecamp assignment + - basecamp notification + - basecamp account # Common actions - link to basecamp - track in basecamp @@ -54,8 +59,12 @@ triggers: - my schedule - my basecamp - assigned to me + - my assignments + - my notifications - overdue todos - upcoming events + - project gauge + - project progress # URLs - 3.basecamp.com - basecampapi.com @@ -66,7 +75,7 @@ argument-hint: "[action] [args...]" # /basecamp - Basecamp Workflow Command -Full CLI coverage: 130 endpoints across todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, and chat. +Full CLI coverage: 155 endpoints across todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, chat, gauges, assignments, notifications, and accounts. ## Agent Invariants @@ -87,7 +96,7 @@ Full CLI coverage: 130 endpoints across todos, cards, messages, files, schedule, - Inline images in descriptions work when attached via `--attach`, not when referenced by URL For todos, documents, and cards, content is sent as-is — use plain text or HTML directly. -6. **Project scope is mandatory for most commands** — via `--in ` or `.basecamp/config.json`. Cross-project exceptions: `basecamp reports assigned` for assigned work, `basecamp reports overdue` for overdue todos, `basecamp reports schedule` for upcoming schedule across all projects, `basecamp recordings ` for browsing by type. +6. **Project scope is mandatory for most commands** — via `--in ` or `.basecamp/config.json`. Cross-project exceptions: `basecamp reports assigned` for assigned work, `basecamp assignments` for structured assignment views, `basecamp reports overdue` for overdue todos, `basecamp reports schedule` for upcoming schedule across all projects, `basecamp recordings ` for browsing by type, `basecamp notifications` for notifications, `basecamp gauges list` for account-wide gauges. ### Output Modes @@ -140,7 +149,7 @@ basecamp --page 1 # First page only, no auto-pagination ## Quick Reference -> **Note:** Most queries require project scope (via `--in ` or `.basecamp/config.json`). Cross-project exceptions: `basecamp reports assigned`, `basecamp reports overdue`, `basecamp reports schedule`, `basecamp recordings `. +> **Note:** Most queries require project scope (via `--in ` or `.basecamp/config.json`). Cross-project exceptions: `basecamp reports assigned`, `basecamp assignments`, `basecamp reports overdue`, `basecamp reports schedule`, `basecamp recordings `, `basecamp notifications`, `basecamp gauges list`. | Task | Command | |------|---------| @@ -172,6 +181,15 @@ basecamp --page 1 # First page only, no auto-pagination | Upload file | `basecamp files uploads create [--vault ] --in --json` | | Download file | `basecamp files download --in ` | | Download inline attachment | `basecamp files download "https://storage.3.basecamp.com/.../download/report.pdf"` | +| My assignments | `basecamp assignments --json` (priorities + non-priorities) | +| Overdue assignments | `basecamp assignments due overdue --json` | +| Completed assignments | `basecamp assignments completed --json` | +| Notifications | `basecamp notifications --json` | +| Mark notification read | `basecamp notifications read --json` | +| Gauges (account-wide) | `basecamp gauges list --json` | +| Gauge needles | `basecamp gauges needles --in --json` | +| Create needle | `basecamp gauges create --position 75 --color green --in --json` | +| Account details | `basecamp accounts show --json` | | Watch timeline | `basecamp timeline --watch` | ## URL Parsing @@ -210,7 +228,10 @@ basecamp comment 123 "Reply" --in Need to find something? ├── Know the type + project? → basecamp list --in --json │ (some groups have default list behavior; use --agent --help if unsure) -├── My assigned work? → basecamp reports assigned --json (defaults to "me") +├── My assigned work? → basecamp assignments --json (priorities + non-priorities) +│ Or: basecamp reports assigned --json (traditional view, defaults to "me") +├── My overdue assignments? → basecamp assignments due overdue --json +├── My notifications? → basecamp notifications --json ├── Upcoming schedule? → basecamp reports schedule --json (cross-project) ├── Overdue across projects? → basecamp reports overdue --json ├── Browse by type cross-project? → basecamp recordings --json @@ -628,6 +649,63 @@ basecamp lineup delete **Note:** Lineup markers are account-wide, not project-scoped. +### Gauges + +Gauges track project progress with colored needles on a 0-100 scale. + +```bash +basecamp gauges list --json # All gauges (account-wide) +basecamp gauges needles --in --json # Needles for a project +basecamp gauges needle --json # Needle details +basecamp gauges create --position 75 --color green --in +basecamp gauges create --position 50 --color yellow --description "Halfway" --in +basecamp gauges create --position 25 --notify custom --subscriptions 1,2 --in +basecamp gauges update --description "Updated" +basecamp gauges delete +basecamp gauges enable --in # Enable gauge on project +basecamp gauges disable --in # Disable gauge +``` + +**Colors:** green, yellow, red. **Notify:** everyone, working_on, custom (with `--subscriptions`). + +### Assignments + +View your assignments across all projects. Separate from `reports assigned` — provides structured priority grouping and due-date scoping. + +```bash +basecamp assignments --json # All (priorities + non-priorities) +basecamp assignments list --json # Same as bare +basecamp assignments completed --json # Completed assignments +basecamp assignments due overdue --json # Overdue +basecamp assignments due due_today --json # Due today +basecamp assignments due due_tomorrow --json # Due tomorrow +basecamp assignments due due_later_this_week --json # Due later this week +``` + +**Scopes:** overdue, due_today, due_tomorrow, due_later_this_week, due_next_week, due_later. + +### Notifications + +```bash +basecamp notifications --json # List (page 1) +basecamp notifications list --page 2 --json # Page 2 +basecamp notifications read --json # Mark as read +basecamp notifications read --page 2 --json # Mark from page 2 +``` + +**Note:** `read` resolves notification IDs from the specified page. Use `--page` to match the page you listed. + +### Accounts + +```bash +basecamp accounts list --json # List authorized accounts +basecamp accounts use # Set default account +basecamp accounts show --json # Account details, limits, subscription +basecamp accounts update --name "New Name" --json # Rename account +basecamp accounts logo upload --json # Upload logo (PNG/JPEG/GIF/WebP/AVIF/HEIC, 5MB max) +basecamp accounts logo remove --json # Remove logo +``` + ### Chat ```bash