Skip to content

feat!: migrate completed command to getAllCompletedTasks endpoint - #204

Open
scottlovegrove wants to merge 3 commits into
mainfrom
scottl/completed-tasks
Open

feat!: migrate completed command to getAllCompletedTasks endpoint#204
scottlovegrove wants to merge 3 commits into
mainfrom
scottl/completed-tasks

Conversation

@scottlovegrove

Copy link
Copy Markdown
Collaborator

Summary

  • Switches td completed from getCompletedTasksByCompletionDate (cursor-based) to getAllCompletedTasks (offset-based)
  • Adds --label, --offset, --annotate-notes, and --annotate-items flags
  • Uses inline project/section data from the response instead of a separate getProjects() call (still fetches full projects only for collaborator resolution when tasks have assignees)

Breaking Changes

  • --cursor flag removed — use --offset instead
  • --all flag removed — offset-based pagination risks 429 rate-limit errors with unbounded fetches

Test plan

  • Type-check passes
  • All 1129 tests pass (18 in completed suite — 13 updated, 5 new)
  • Lint clean
  • Format clean
  • Manual: td completed shows today's completed tasks
  • Manual: td completed --label "work" filters by label
  • Manual: td completed --offset 10 --limit 5 pagination works
  • Manual: td completed --annotate-notes --json includes comment data

🤖 Generated with Claude Code

@scottlovegrove scottlovegrove self-assigned this Mar 30, 2026
@scottlovegrove
scottlovegrove force-pushed the scottl/completed-tasks branch from e36e378 to f8bfd3b Compare April 1, 2026 10:20
@scottlovegrove
scottlovegrove force-pushed the scottl/completed-tasks branch 3 times, most recently from 672ec74 to 34317ec Compare April 13, 2026 20:32
@scottlovegrove

This comment was marked as outdated.

@scottlovegrove

Copy link
Copy Markdown
Collaborator Author

Reopening — closed this in error. My local branch was stale and I misread the commit graph.

@scottlovegrove
scottlovegrove force-pushed the scottl/completed-tasks branch 2 times, most recently from a16356e to c98c31d Compare April 20, 2026 15:22
scottlovegrove and others added 3 commits April 22, 2026 16:51
Switch from getCompletedTasksByCompletionDate (cursor-based) to
getAllCompletedTasks (offset-based) for richer filtering and inline
project/section data. Adds --label, --offset, --annotate-notes, and
--annotate-items flags.

BREAKING CHANGE: --cursor and --all flags are removed. Use --offset
for pagination instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Main made formatTaskRow async; adapt the new listByDate path so it
awaits the row renderer instead of logging a Promise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@scottlovegrove
scottlovegrove force-pushed the scottl/completed-tasks branch from c98c31d to 2c5b017 Compare April 22, 2026 15:51
@scottlovegrove
scottlovegrove marked this pull request as ready for review July 13, 2026 09:36
@doistbot
doistbot requested a review from engfragui July 13, 2026 09:36

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR migrates the td completed command from the cursor-based getCompletedTasksByCompletionDate endpoint to the offset-based getAllCompletedTasks endpoint, adding --label, --offset, --annotate-notes, and --annotate-items flags while removing --all and --cursor from the date-based path.

Few things worth tightening:

  • Legacy flags silently ignored: --all and --cursor are still registered on the command but never read by listByDate, so users running td completed --all get a truncated default-limit result with no warning. These should either be rejected with an error in the date path (as the search path already does for incompatible flags) or removed from the definition.

  • Date boundary excludes end-date tasks: until is set to midnight (T00:00:00) on the specified end date, excluding any tasks completed later that day. Using T23:59:59 or advancing by one day would fix this.

  • New options silently dropped in search path: --label, --offset, --annotate-notes, and --annotate-items are accepted by Commander but ignored when --search is used, returning unfiltered/unannotated results without error.

  • Annotation data stripped in JSON output: --annotate-notes --json (and the same for --annotate-items / NDJSON) drops annotation fields unless --full is also passed, since formatPaginatedJson strips non-essential fields. Annotation flags should preserve their fields or imply full output.

  • Duplicated output blocks: listByDate and listSearchResults now have nearly identical JSON/NDJSON/text formatting blocks. Extracting a shared helper would keep output-format changes in one place.

  • Missing default-limit test coverage: The default limit changed from 300 to a hardcoded 30, but no test asserts this — adding limit: 30 to the existing expect.objectContaining would guard the offset-hint behavior that depends on it.

  • Minor type-safety and efficiency notes: The double cast on resp.projects could be narrowed to Record<string, { name: string }>, and the redundant getProjects() call (fired once for name resolution and again for assignee resolution) could be reused.

I also included a few optional follow-up notes in the details below.

Optional follow-up notes (2)
  • P3 src/commands/completed/list.ts:190: resp.projects as Record<string, Record<string, unknown>> followed by inlineProjects[pid]?.name as string | undefined is a double cast that bypasses type safety. If the SDK doesn't type the projects field, a narrower cast like Record<string, { name: string }> would eliminate the second cast and give compile-time safety on the accessed shape.
  • P3 src/commands/completed/list.ts:172: When --project is passed as a project name (not id: or URL), resolveProjectIdresolveProjectRef calls api.getProjects() to fuzzy-match the name, then discards the full project list. Later, if any returned tasks have assignees (line 228), api.getProjects() is called a second time to build the project map for collaborator resolution. The first call's result could be reused instead of refetching. This only fires in the narrow path of name-based project resolution + assignees, so it's low priority.

Share FeedbackReview Logs


const api = await getApi()

if (isSearch) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 --all and --cursor are silently ignored in the date-based path. Both flags are still defined on the command in index.ts and accepted by Commander, but listByDate never reads options.all or options.cursor. A user running td completed --all (without --search) expects all results, but gets at most the default limit of 30. This is a regression from the old code where --all worked correctly via cursor-based pagination. The search path already validates incompatible flags (e.g., --since with --search throws an error); the date path should follow the same pattern and reject --all and --cursor when not using --search. Alternatively, the flags could be removed from the command definition, but they are still needed for the search path.


const resp = await api.getAllCompletedTasks({
since: new Date(since + 'T00:00:00'),
until: new Date(until + 'T00:00:00'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 until is set to midnight at the start of the specified date (new Date(until + 'T00:00:00')). This creates a range that excludes tasks completed on the until date after midnight. For example, td completed --since 2024-01-08 --until 2024-01-08 sends identical timestamps for both since and until (Jan 8 00:00:00), producing a zero-width range that returns no tasks. Even for multi-day ranges like --since 2024-01-01 --until 2024-01-08, tasks completed on Jan 8 are excluded. The default case works because until defaults to tomorrow. Fix: use T23:59:59 for the until timestamp, or advance until by one day.


const api = await getApi()

if (isSearch) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 This early search branch silently ignores the newly accepted --label, --offset, --annotate-notes, and --annotate-items options: listSearchResults only sends query/cursor/limit. A command such as td completed --search foo --label urgent returns unfiltered results. Reject these incompatible combinations (as is already done for date/project options), or implement them for search.

responsibleName: getAssigneeName(task),
}))
console.log(
formatPaginatedJson(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 --annotate-notes --json does not include the requested annotation data unless the user also adds --full. formatPaginatedJson strips non-essential task fields by default, and the task essential-field list does not include annotation/comment fields; the same applies to NDJSON and --annotate-items. Preserve annotation fields when either annotation flag is set, or make those flags imply full output.

})
}

if (options.json) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 The output blocks in listByDate (lines 244–274 for JSON/NDJSON, 276–298 for text) are structurally identical to listSearchResults (lines 118–148 for JSON/NDJSON, 150–163 for text). Both functions map tasks to add responsibleName, call the same formatPaginatedJson/formatPaginatedNdjson/formatTaskRow functions, and differ only in nextCursor (variable vs null) and the footer message.

The pre-PR code used a single function with inline isSearch branches, which avoided this duplication. Consider extracting a shared helper that accepts the task list, a project-name resolver, an assignee-name resolver, the nextCursor value, and a footer string, so that output-format changes only need to be made once.

expect.objectContaining({
since: getToday(),
until: getTomorrow(),
since: new Date(getToday() + 'T00:00:00'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 The default limit changed from LIMITS.tasks (300) to a hardcoded 30 in listByDate, but no test verifies this default. The --uses today as default since date test calls getAllCompletedTasks without --limit and uses expect.objectContaining — it checks since/until but not limit. Since the new offset-hint feature triggers when tasks.length === limit, a regression in the default value would silently change hint behavior. Adding limit: 30 to the existing expect.objectContaining in this test would cover it.

@engfragui

engfragui commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@scottlovegrove The command doesn't seem to actually work after this PR the command, i.e. returns a bunch of "Invalid input: expected string/object/bool/etc., received undefined" errors.

Should be because @doist/todoist-sdk's getAllCompletedTasks runs validateTaskArray against the full strict Task schema, but the completed endpoint returns a partial shape (parentId, labels, priority, description, etc. all undefined; addedAt/updatedAt as Invalid Date), so validation throws before anything renders.

I'll let you take a look with no rush.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants