feat!: migrate completed command to getAllCompletedTasks endpoint - #204
feat!: migrate completed command to getAllCompletedTasks endpoint#204scottlovegrove wants to merge 3 commits into
Conversation
e36e378 to
f8bfd3b
Compare
672ec74 to
34317ec
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
Reopening — closed this in error. My local branch was stale and I misread the commit graph. |
a16356e to
c98c31d
Compare
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>
c98c31d to
2c5b017
Compare
doistbot
left a comment
There was a problem hiding this comment.
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:
--alland--cursorare still registered on the command but never read bylistByDate, so users runningtd completed --allget 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:
untilis set to midnight (T00:00:00) on the specified end date, excluding any tasks completed later that day. UsingT23:59:59or advancing by one day would fix this. -
New options silently dropped in search path:
--label,--offset,--annotate-notes, and--annotate-itemsare accepted by Commander but ignored when--searchis 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--fullis also passed, sinceformatPaginatedJsonstrips non-essential fields. Annotation flags should preserve their fields or imply full output. -
Duplicated output blocks:
listByDateandlistSearchResultsnow 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 — addinglimit: 30to the existingexpect.objectContainingwould guard the offset-hint behavior that depends on it. -
Minor type-safety and efficiency notes: The double cast on
resp.projectscould be narrowed toRecord<string, { name: string }>, and the redundantgetProjects()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)
src/commands/completed/list.ts:190:
resp.projects as Record<string, Record<string, unknown>>followed byinlineProjects[pid]?.name as string | undefinedis a double cast that bypasses type safety. If the SDK doesn't type theprojectsfield, a narrower cast likeRecord<string, { name: string }>would eliminate the second cast and give compile-time safety on the accessed shape.src/commands/completed/list.ts:172: When
--projectis passed as a project name (notid:or URL),resolveProjectId→resolveProjectRefcallsapi.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.
|
|
||
| const api = await getApi() | ||
|
|
||
| if (isSearch) { |
There was a problem hiding this comment.
--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'), |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
--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) { |
There was a problem hiding this comment.
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'), |
There was a problem hiding this comment.
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.
|
@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 I'll let you take a look with no rush. |
Summary
td completedfromgetCompletedTasksByCompletionDate(cursor-based) togetAllCompletedTasks(offset-based)--label,--offset,--annotate-notes, and--annotate-itemsflagsgetProjects()call (still fetches full projects only for collaborator resolution when tasks have assignees)Breaking Changes
--cursorflag removed — use--offsetinstead--allflag removed — offset-based pagination risks 429 rate-limit errors with unbounded fetchesTest plan
td completedshows today's completed taskstd completed --label "work"filters by labeltd completed --offset 10 --limit 5pagination workstd completed --annotate-notes --jsonincludes comment data🤖 Generated with Claude Code