Skip to content

feat(status): add tdc status show, set and clear - #77

Merged
lmjabreu merged 12 commits into
mainfrom
lmjabreu/status-command
Oct 14, 2026
Merged

lmjabreu merged 12 commits into
mainfrom
lmjabreu/status-command

Conversation

@lmjabreu

@lmjabreu lmjabreu commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Adds tdc status to show, set and clear your shared status (message, emoji, optional expiry, optional pause notifications). It uses users.setStatus() and users.clearStatus() from @doist/comms-sdk 3.4.0, which this bumps to. tdc user also prints a Status: line when one is set.

set and clear need the user:write scope, so they need tdc auth login --full-access. tdc status works on a default login. --until takes YYYY-MM-DD or YYYY-MM-DDTHH:MM (local time, or add Z or an offset) and --for takes a duration such as 30m, 2h or 1h30m. Impossible dates, past expiries and messages outside 1 to 80 characters are refused before anything is sent, --dry-run included.

Also removes the tdc away lines from the README, since that command no longer exists.

Reference

Adversarial review

Reviewer Head Raised Fixed Refuted Deferred Reviewer tokens
Fable, 4 passes 8bce083 25 18 4 3 708,126
Fable, 2 passes (revision) 265afea 10 8 1 1 380,404

Doistbot: filled in at merge.

Test plan

  1. Log in with tdc auth login --full-access and run tdc status.
    • It prints No status set.
  2. Run tdc status set "Deep work" --emoji 🎯 --for 2h --pause-notifications.
    • It prints the message with the expiry and notifications paused.
  3. Run tdc status and tdc user.
    • Both show the status you set.
  4. Run tdc status set "x" --until 2026-02-30, then --until 2020-01-01, then --for soon, each with and without --dry-run.
    • Each fails with an invalid date or value error and nothing is sent.
  5. Run tdc status set "x" --until 2030-01-01 --for 2h.
    • It fails with Cannot use --until and --for together.
  6. Run tdc status clear, then tdc status.
    • It prints Status cleared., then No status set.
  7. Log in with the default tdc auth login and run tdc status set "x".
    • It is refused with a message naming user:write and --full-access, before a request is made.
  8. Log in with --read-only and run tdc status set "x".
    • It is refused as a read-only login.

🤖 Generated with Claude Code

@lmjabreu
lmjabreu marked this pull request as ready for review October 13, 2026 21:49
@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

@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.

Adds a tdc status command for showing, setting, and clearing your shared status, backed by the new users.setStatus()/users.clearStatus() SDK methods, with --emoji, --until/--for expiry options, notification pausing, and a Status: line in tdc user.

Few things worth tightening:

  • tdc status set "Focus" --for "" --pause-notifications currently slips past validation and pauses notifications indefinitely — check option presence with !== undefined (for --until, --for, and their mutual-exclusion check) so explicitly empty values reach validation and get rejected.

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

Optional follow-up notes (7)
  • P3 src/lib/duration.ts:1: UNIT_MS redefines the same millisecond constants that src/lib/dates.ts already computes (MINUTE, HOUR, DAY). Export those constants from dates.ts (or move them into a shared time-unit module) and reuse them here instead of maintaining duplicate values that can drift.
  • P3 src/commands/status/index.ts:28: The help text says "local time without an offset", but parseDate falls through to new Date(dateStr), and ECMAScript parses a date-only string like --until 2026-10-20 as UTC midnight, not local time — a user at UTC-8 gets an expiry 8 hours earlier than the docs imply. Either extend parsing so date-only values use local midnight, or adjust the wording to say date-only values are interpreted as UTC.
  • P3 src/index.ts:57: status never renders markdown (show/set/clear only print plain text or JSON), but it isn't in noMarkdownCommands, so every plain-text tdc status run blocks on preloadMarkdown() before parsing. Add 'status' to the noMarkdownCommands set in src/index.ts to skip that startup cost.
  • P3 src/commands/status/status.test.ts:43: vi.useFakeTimers() is never undone. Add afterEach(() => vi.useRealTimers()) (importing afterEach) so the suite doesn't leave fake timers enabled once it finishes. src/lib/dates.test.ts already follows this cleanup pattern for the same setup.
  • P3 src/commands/status/status.test.ts:100: This test verifies the computed activeUntil sent to the SDK, but no test asserts tdc status set's human-readable output. The PR's test-plan case (--for 2h --pause-notifications) should print the expiry and notifications paused; have the setStatus mock return a status with activeUntil and effects, then assert the logged text contains until and notifications paused.
  • P3 src/commands/user.ts:40: The new tdc user Status: line has no test coverage. Add a case in src/commands/user.test.ts where getSessionUser resolves a user with status and assert the text output includes the formatted Status: line; optionally assert the line is omitted when status is absent.
  • P3 src/commands/status/index.ts:33: Per AGENTS.md ("JSON output on mutating commands"), commands that return an object from the API (create/update) should also support --full. status set and status clear return the updated user from the API but only register --json. Consider adding .option('--full', 'Include all fields in JSON output') and passing it through to formatJson(..., 'user', options.full) in set.ts/clear.ts, matching how tdc user and groups create handle it.

Share Feedback • Review Logs

Comment thread src/commands/status/set.ts Outdated
@lmjabreu

Copy link
Copy Markdown
Contributor Author

The P3 notes from the review body, all addressed in 182643f:

  • Duplicated unit constants: Done. dates.ts now exports them and duration.ts imports them.
  • --until 2026-10-20 read as UTC: Fixed by parsing a date-only value as local midnight instead of rewording the help.
  • status missing from noMarkdownCommands: Done.
  • Fake timers never reset: Done, added afterEach(useRealTimers).
  • No test for the set text output: Done.
  • No test for the tdc user Status line: Done.
  • --full on set and clear: Done, they return the full user with it.

@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

@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 adds tdc status with show/set/clear subcommands backed by users.setStatus()/users.clearStatus() in comms-sdk 3.4.0, plus a Status: line in tdc user. The implementation follows existing conventions well (MutationOptions, spinner messages, scope mapping, read-only guard) and addresses prior review feedback.

Few things worth tightening:

  • Accept fractional seconds in --until ISO timestamps — the current regex rejects 2026-10-20T09:00:00.000Z (the exact format Date.toISOString() produces), breaking scripts that pass generated timestamps; preserve milliseconds when constructing the date and add a test.
  • status clear --json doesn't propagate a non-zero exit code on failure.
  • The user-status JSON rendering block is duplicated across the show, set, and clear handlers — consider moving it into the shared helpers.ts.
  • parseLocalDate silently shifts local times that fall in a DST gap; worth a note or explicit handling.

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

Optional follow-up notes (5)
  • P3 src/commands/status/clear.ts:24: --json bypasses the failure exit code. The user.status re-check sits after the JSON return, so when clearStatus() still returns a status, tdc status clear --json prints { status: ... } (or the full user) and exits 0, while text mode exits 1. Move this check above the JSON branch, or throw a CliError, so scripted callers get a non-zero exit consistently.
  • P3 src/lib/dates.ts:85: Nonexistent local times during DST gaps are accepted with a shifted instant. new Date(y, mo - 1, d, h, mi, s) normalizes e.g. 2026-03-08T02:30 in America/New_York to a neighboring hour, and the month/day check still passes, so --until silently sets an expiry one hour off instead of rejecting it. Add local.getHours() === h && local.getMinutes() === mi && local.getSeconds() === s to this branch to catch DST gaps.
  • P3 src/commands/status/set.ts:104: The --json/--full status rendering block is duplicated verbatim in show.ts, set.ts, and clear.ts. status/helpers.ts already exists as the shared home for status formatting, so add a helper there (e.g. formatStatusJson(user, full)) and call it from all three handlers instead of repeating the options.full ? formatJson(user, 'user', true) : formatJson({ status: user.status ?? null }) branch.
  • P3 src/commands/status/set.ts:112: When setStatus() returns no status, this path logs a caveat but leaves the exit code 0 in both text and JSON modes, so a script can't detect that the status wasn't actually set; clear already exits 1 in the equivalent case. Throw a CliError (or otherwise set a non-zero exit) so the failure is explicit and consistent.
  • P3 src/lib/permissions.test.ts:86: These cases verify the user:write scope mapping, but no test proves users.setStatus/users.clearStatus are actually routed through the write guard and refused in read-only mode. isMutatingMethod's mutating assertions don't list either method, and the command tests mock getCommsClient, bypassing the proxy guard entirely. Add both methods to the identifies mutating methods assertions (or add an api.test.ts routing test like the groups.addUsers case) so a future addition to KNOWN_SAFE_API_METHODS can't silently allow status writes.

Share Feedback • Review Logs

Comment thread src/lib/dates.ts Outdated
@lmjabreu

Copy link
Copy Markdown
Contributor Author

The P3 notes from the latest review body, addressed in 761dc90:

  • clear --json exiting 0 with a status still set: Fixed. The check runs before the JSON branch, so both modes exit 1.
  • DST gap in parseLocalDate: Fixed. A local time that doesn't exist is rejected.
  • Duplicated JSON rendering: Done. One formatStatusJson helper in helpers.ts.
  • set exiting 0 when the server returns no status: Fixed. It exits 1 in text and JSON modes, like clear.
  • No read-only routing assertion: Done. Both methods are in the identifies mutating methods assertions.

@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

@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.

Adds tdc status with show, set, and clear subcommands on top of the new users.setStatus/users.clearStatus SDK methods, including duration/date-based expiry, notification pausing, and upfront validation — plus a status line in tdc user and a README cleanup. No inline issues were flagged.

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

Optional follow-up notes (4)
  • P3 src/commands/user.ts:14: formatStatus is no longer just a status-subcommand helper now that tdc user uses it, so importing it from ./status/helpers.js couples two sibling command folders and makes commands/status a shared dependency of user. Move the shared status formatting helper into src/lib (e.g. src/lib/output.ts next to the other formatting utilities) and import it from both commands, keeping the command folders self-contained per the helpers.ts pattern in AGENTS.md.
  • P3 src/commands/status/set.ts:72: The hint assumes a date-only input, but this branch also fires for past datetimes: tdc status set "x" --until 2020-01-01T00:00:00Z fails with "A date with no time means midnight at its start, so add a time or use a later date", and adding a time won't help there. Generalize the hint (e.g. "Use a future date or datetime") or emit the midnight-specific hint only when no time was given.
  • P3 src/commands/user.test.ts:181: The PR adds status to USER_ESSENTIAL_FIELDS in src/lib/output.ts, so tdc user --json now returns the status in curated output without --full, but no test asserts that contract. The new user status line block only covers text output. Add a case where getSessionUser resolves a user with status and assert the parsed --json output includes status; this guards against the field silently dropping out of USER_ESSENTIAL_FIELDS.
  • P3 src/commands/status/status.test.ts:211: The beforeEach in tdc status set input guards (and the fake-timer parts of tdc status) duplicate the one in tdc status verbatim: clearAllMocks, useFakeTimers + setSystemTime(NOW), createUsersClient(), and the getCommsClient mock. Extract a shared setup helper (e.g. function setupStatusFixture() { ... return users }) and call it from each describe so the client/timer wiring stays in one place when the fixture changes.

Share Feedback • Review Logs

@lmjabreu

Copy link
Copy Markdown
Contributor Author

The P3 notes from the latest review body, addressed in c4053f7:

  • formatStatus imported across command folders: Done. It now lives in src/lib/status.ts and both commands import it from there.
  • The midnight hint on a past datetime: Fixed. The hint only appears for a date with no time; a past datetime gets "Use a date or time in the future".
  • No test for status in tdc user --json: Done. A test pins it in the curated output, with and without a status.
  • Duplicated beforeEach setup in the status tests: Keeping as-is. Each describe resets its own timers and mocks, and a shared helper would hide that for about five lines.

Comment thread skills/comms-cli/SKILL.md
Comment thread src/commands/status/index.ts Outdated
Comment thread src/commands/status/set.ts Outdated
lmjabreu and others added 12 commits October 15, 2026 00:28
Needs a @doist/comms-sdk release that includes users.setStatus (Doist/comms-sdk-typescript#100);
the dependency bump is not in this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brings in users.setStatus and users.clearStatus.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The command no longer exists.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… local

Addresses doistbot review: validate --until/--for presence instead of
truthiness, skip markdown preload for status, add --full to set/clear,
reuse the dates.ts unit constants, and cover the user Status line, the
set text output and fake-timer cleanup in tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t what the server returned

From an offline review of the whole PR: reject impossible dates, past
expiries, out-of-range messages and empty emoji before the dry-run branch,
require user:write through the scope table, print the server's status after
set and clear, add --full to show, and fix the stale AGENTS.md pointer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Commander reads the next token as the value, so --emoji --dry-run ran the
real write. An emoji never starts with a dash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the server disagrees

Addresses the second doistbot round: parseLocalDate takes what
Date.toISOString() produces, a nonexistent local time in a daylight-saving
gap is rejected, clear and set exit 1 whenever the server's status does not
match the request (JSON output included), the JSON rendering lives in one
helper, and the read-only routing is asserted for both status methods.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…and offset range, fail through CliError

From a revision review of 761dc90: the DST-gap rule rejected a date-only
--until on the one day a year when a zone's clocks skip midnight, years 0 to
99 round-tripped to 1900 to 1999, and offsets up to +23:59 were accepted.
set and clear now throw INVALID_STATE when the server disagrees, so --json
prints the usual stderr envelope and no process.exitCode state leaks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tus, cover the curated user JSON

From the third doistbot round: the midnight hint now appears only for a date
with no time, formatStatus moves to src/lib so tdc user no longer imports
from the status folder, and a test pins status in the curated tdc user --json
output.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the no-op error

From Scott's review: the help, README and skill text now say the pause holds
back Comms notifications only, that each app has to read the status, and that
held notifications are not sent later. An agent is told to pass the flag only
when asked. The set error now says nothing changed, the usual cause, and what
to run next.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The status commands print the user through formatJson, which since #78
drops token and cometChannel. This guards the call site.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@lmjabreu
lmjabreu force-pushed the lmjabreu/status-command branch from 8ee5cb1 to 5ddd06f Compare October 14, 2026 23:29
@lmjabreu
lmjabreu merged commit bdfcb16 into main Oct 14, 2026
6 checks passed
@lmjabreu
lmjabreu deleted the lmjabreu/status-command branch October 14, 2026 23:37
doist-release-bot Bot added a commit that referenced this pull request Oct 14, 2026
## [3.6.0](v3.5.2...v3.6.0) (2026-10-14)

### Features

* **status:** add tdc status show, set and clear ([#77](#77)) ([bdfcb16](bdfcb16)), closes [Doist/comms-sdk-typescript#100](Doist/comms-sdk-typescript#100) [#78](#78)
@doist-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.6.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants