Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -3479,6 +3479,7 @@ FLAG basecamp checkin answer update --verbose type=count
FLAG basecamp checkin answers --account type=string
FLAG basecamp checkin answers --agent type=bool
FLAG basecamp checkin answers --all type=bool
FLAG basecamp checkin answers --by type=string
FLAG basecamp checkin answers --cache-dir type=string
FLAG basecamp checkin answers --count type=bool
FLAG basecamp checkin answers --help type=bool
Expand Down Expand Up @@ -3747,6 +3748,7 @@ FLAG basecamp checkins answer update --verbose type=count
FLAG basecamp checkins answers --account type=string
FLAG basecamp checkins answers --agent type=bool
FLAG basecamp checkins answers --all type=bool
FLAG basecamp checkins answers --by type=string
FLAG basecamp checkins answers --cache-dir type=string
FLAG basecamp checkins answers --count type=bool
FLAG basecamp checkins answers --help type=bool
Expand Down
38 changes: 33 additions & 5 deletions internal/commands/checkins.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@
var limit int
var page int
var all bool
var by string

cmd := &cobra.Command{
Use: "answers <question_id|url>",
Expand All @@ -536,7 +537,11 @@

You can pass either a question ID or a Basecamp URL:
basecamp checkins answers 789 --in my-project
basecamp checkins answers https://3.basecamp.com/123/buckets/456/questions/789`,
basecamp checkins answers https://3.basecamp.com/123/buckets/456/questions/789

Use --by to filter answers by a specific person (name, email, ID, or "me"):
basecamp checkins answers 789 --by me --in my-project
basecamp checkins answers 789 --by "Alice Smith" --in my-project`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
app := appctx.FromContext(cmd.Context())
Expand Down Expand Up @@ -598,11 +603,33 @@
opts.Page = page
}

answersResult, err := app.Account().Checkins().ListAnswers(cmd.Context(), questionID, opts)
if err != nil {
return convertSDKError(err)
trimmedBy := strings.TrimSpace(by)
if by != "" && trimmedBy == "" {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Treat explicitly empty --by values as invalid; this condition currently misses --by= and falls back to the unfiltered endpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/commands/checkins.go, line 607:

<comment>Treat explicitly empty `--by` values as invalid; this condition currently misses `--by=` and falls back to the unfiltered endpoint.</comment>

<file context>
@@ -603,9 +603,14 @@ Use --by to filter answers by a specific person (name, email, ID, or "me"):
 			}
 
+			trimmedBy := strings.TrimSpace(by)
+			if by != "" && trimmedBy == "" {
+				return output.ErrUsage("--by value cannot be blank")
+			}
</file context>
Fix with Cubic

return output.ErrUsage("--by value cannot be blank")
}

var answers []basecamp.QuestionAnswer
if trimmedBy != "" {
personIDStr, _, err := app.Names.ResolvePerson(cmd.Context(), trimmedBy)
if err != nil {
return err
}
personID, err := strconv.ParseInt(personIDStr, 10, 64)
if err != nil {
return output.ErrUsage("Invalid person ID")
}
answersResult, err := app.Account().Checkins().ListAnswersByUser(cmd.Context(), questionID, personID, opts)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / Integration Tests

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / Security

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / Race Detection

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / Lint

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)) (typecheck)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / Tests

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / CLI Surface Check

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)

Check failure on line 621 in internal/commands/checkins.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

app.Account().Checkins().ListAnswersByUser undefined (type *basecamp.CheckinsService has no field or method ListAnswersByUser)
if err != nil {
return convertSDKError(err)
Comment thread
robzolkos marked this conversation as resolved.
}
answers = answersResult.Answers
} else {
answersResult, err := app.Account().Checkins().ListAnswers(cmd.Context(), questionID, opts)
if err != nil {
return convertSDKError(err)
}
answers = answersResult.Answers
}
answers := answersResult.Answers

return app.OK(answers,
output.WithSummary(fmt.Sprintf("%d answers", len(answers))),
Expand All @@ -625,6 +652,7 @@
cmd.Flags().IntVarP(&limit, "limit", "n", 0, "Maximum number of answers to fetch (0 = all)")
cmd.Flags().BoolVar(&all, "all", false, "Fetch all answers (no limit)")
cmd.Flags().IntVar(&page, "page", 0, "Fetch a single page (use --all for everything)")
cmd.Flags().StringVar(&by, "by", "", "Filter answers by person (name, email, ID, or \"me\")")

return cmd
}
Expand Down
60 changes: 60 additions & 0 deletions internal/commands/checkins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,66 @@ import (
"github.com/stretchr/testify/require"
)

type mockCheckinsAnswersByPersonTransport struct {
recordedPath string
}

func (m *mockCheckinsAnswersByPersonTransport) RoundTrip(req *http.Request) (*http.Response, error) {
header := make(http.Header)
header.Set("Content-Type", "application/json")

switch {
case req.Method == "GET" && strings.Contains(req.URL.Path, "/projects.json"):
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`[{"id":123,"name":"Test Project"}]`)),
Header: header,
}, nil
case req.Method == "GET" && strings.Contains(req.URL.Path, "/people.json"):
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`[{"id":456,"name":"Alice Smith","email_address":"alice@example.com"}]`)),
Header: header,
}, nil
case req.Method == "GET" && strings.Contains(req.URL.Path, "/questions/789/answers/by/456"):
m.recordedPath = req.URL.Path
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`[{
"id": 1001,
"content": "<div>Alice's answer</div>",
"group_on": "2026-04-21",
"creator": {"id": 456, "name": "Alice Smith"},
"parent": {"id": 789, "title": "What did you work on?", "type": "Question", "url": "https://example.test/questions/789", "app_url": "https://example.test/questions/789"},
"bucket": {"id": 123, "name": "Test Project", "type": "Project"},
"status": "active",
"type": "Question::Answer",
"title": "What did you work on?"
}]`)),
Header: header,
}, nil
default:
return &http.Response{
StatusCode: 404,
Body: io.NopCloser(strings.NewReader(`{"error":"Not Found"}`)),
Header: header,
}, nil
}
}

func TestCheckinsAnswersByPersonFlag(t *testing.T) {
transport := &mockCheckinsAnswersByPersonTransport{}
app, _ := newTestAppWithTransport(t, transport)
app.Config.ProjectID = "123"

project := ""
cmd := newCheckinsAnswersCmd(&project)

err := executeCommand(cmd, app, "789", "--by", "Alice Smith")
require.NoError(t, err)
assert.Contains(t, transport.recordedPath, "/questions/789/answers/by/456")
}

type mockCheckinsAnswerCreateTransport struct {
recordedPath string
recordedBody map[string]any
Expand Down
2 changes: 2 additions & 0 deletions skills/basecamp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,8 @@ basecamp checkins --in <project> --json # Questionnaire info
basecamp checkins questions --in <project> # List questions
basecamp checkins question <id> --in <project> # Question details
basecamp checkins answers <question_id> --in <project> # List answers
basecamp checkins answers <question_id> --by me --in <project> # My answers only
basecamp checkins answers <question_id> --by "Alice Smith" --in <project> # Filter by person (name, email, or ID)
basecamp checkins answer <id> --in <project> # Answer details
basecamp checkins question create "What did you work on?" --in <project>
basecamp checkins question update <id> "New question" --frequency every_week
Expand Down
Loading