From 133e44df58e191c9d59f7014c10f5856b02a74b2 Mon Sep 17 00:00:00 2001 From: lmjabreu Date: Fri, 23 Oct 2026 14:17:21 +0100 Subject: [PATCH 1/2] feat(status): add --pause-activity to status set and show it Co-Authored-By: Claude Sonnet 5.5 --- skills/comms-cli/SKILL.md | 3 + src/commands/status/index.ts | 9 ++- src/commands/status/set.ts | 11 +++- src/commands/status/status.test.ts | 96 ++++++++++++++++++++++++++++++ src/commands/user.test.ts | 13 ++++ src/lib/skills/content.ts | 3 + src/lib/status.ts | 3 + 7 files changed, 136 insertions(+), 2 deletions(-) diff --git a/skills/comms-cli/SKILL.md b/skills/comms-cli/SKILL.md index def4551..8f3652f 100644 --- a/skills/comms-cli/SKILL.md +++ b/skills/comms-cli/SKILL.md @@ -258,6 +258,7 @@ tdc users --ids-only # Output only user IDs tdc status # Show your status (default login is enough) tdc status --json # Show it as { status } JSON (null when unset; --full for the whole user) tdc status set "Deep work" --emoji 🎯 --for 2h --pause-notifications # Set it (replaces any current status; needs --full-access) +tdc status set "On holiday" --pause-activity # Pause Todoist Karma and streaks while it is active (independent of --pause-notifications) tdc status set "On holiday" --until 2026-10-20T09:00 # End at a date (YYYY-MM-DD, YYYY-MM-DDTHH:MM or a full ISO 8601 timestamp; local time unless it has Z or an offset) tdc status set "Away" --dry-run # Preview without setting tdc status set "Away" --json # Set and return { status } as JSON @@ -324,6 +325,8 @@ All group *writes* (`groups create`, `rename`, `delete`, `add-user`, `remove-use Only pass `--pause-notifications` when the user asks for it. It holds back Comms notifications only for now: each app has to read the status and hold its own notifications back, and Todoist does not yet. Held notifications are not sent later. +Only pass `--pause-activity` when the user asks for it. It pauses their Todoist Karma and streaks while the status is active. It is independent of `--pause-notifications`: it does not hold back notifications, and pausing notifications does not protect Karma. + If a channel is not found in `tdc channels`, widen with broader listings such as `tdc channels --scope public`, then `tdc channels --scope public --state all`. Check `tdc channels --help` for other available filters. `tdc channel threads` returns every thread in the channel; pagination filters (`--limit`, `--cursor`, `--since`, `--until`, `--unread`) are applied client-side after fetch. `--archive-filter` is applied server-side. Results are sorted newest-first by last activity. In `--json` / `--ndjson`, the response includes a `nextCursor` string (opaque) you can pass via `--cursor` to fetch the next page; NDJSON emits the cursor as a final `{ "_meta": true, "nextCursor": "..." }` line. diff --git a/src/commands/status/index.ts b/src/commands/status/index.ts index 51d534c..5e78ab5 100644 --- a/src/commands/status/index.ts +++ b/src/commands/status/index.ts @@ -33,6 +33,10 @@ Examples: '--pause-notifications', 'Hold back Comms notifications while the status is active. Comms only for now: each app has to read the status to honour it', ) + .option( + '--pause-activity', + 'Pause your Todoist Karma and streaks while the status is active. Independent of --pause-notifications', + ) .option('--dry-run', 'Show what would be set without setting it') .option('--json', 'Output as JSON') .option('--full', 'Include all user fields in JSON output') @@ -43,10 +47,13 @@ The message is 1 to 80 characters. The status is shared with your Todoist accoun Setting or clearing it needs the user:write scope, so log in with tdc auth login --full-access. --pause-notifications only affects Comms for now. Each app has to read the status and hold its own notifications back, and Todoist does not yet. Held notifications are not sent later. +--pause-activity protects your Todoist Karma and streaks. It does not hold back notifications, +and --pause-notifications does not protect Karma, so pass both if you want both. Examples: tdc status set "On holiday" --emoji 🏝️ --until 2026-10-20T09:00 - tdc status set "Deep work" --emoji 🎯 --for 2h --pause-notifications`, + tdc status set "Deep work" --emoji 🎯 --for 2h --pause-notifications + tdc status set "On holiday" --emoji 🏝️ --until 2026-10-30 --pause-activity`, ) .action(setStatus) diff --git a/src/commands/status/set.ts b/src/commands/status/set.ts index a321423..54d6e4d 100644 --- a/src/commands/status/set.ts +++ b/src/commands/status/set.ts @@ -15,6 +15,7 @@ type SetStatusOptions = MutationOptions & { until?: string for?: string pauseNotifications?: boolean + pauseActivity?: boolean } function resolveMessage(message: string): string { @@ -89,7 +90,14 @@ export async function setStatus(message: string, options: SetStatusOptions): Pro message: text, ...(emoji ? { emoji } : {}), ...(activeUntil ? { activeUntil } : {}), - ...(options.pauseNotifications ? { effects: { pauseNotifications: {} } } : {}), + ...(options.pauseNotifications || options.pauseActivity + ? { + effects: { + ...(options.pauseNotifications ? { pauseNotifications: {} } : {}), + ...(options.pauseActivity ? { pauseActivity: {} } : {}), + }, + } + : {}), } if (options.dryRun) { @@ -98,6 +106,7 @@ export async function setStatus(message: string, options: SetStatusOptions): Pro Emoji: emoji, Until: activeUntil ? formatDateTime(activeUntil) : undefined, 'Pause notifications': options.pauseNotifications ? 'yes' : undefined, + 'Pause activity': options.pauseActivity ? 'yes' : undefined, }) return } diff --git a/src/commands/status/status.test.ts b/src/commands/status/status.test.ts index 9aa36e2..baa49f5 100644 --- a/src/commands/status/status.test.ts +++ b/src/commands/status/status.test.ts @@ -64,6 +64,46 @@ describe('tdc status', () => { expect(logged(spy)).toBe('🎯 Deep work (notifications paused)') }) + it('names each paused effect, notifications first', async () => { + apiMocks.getSessionUser.mockResolvedValue({ + status: { + message: 'Holiday', + effects: { + pauseActivity: { scope: { mode: 'all' } }, + pauseNotifications: { scope: { mode: 'all' } }, + }, + }, + }) + const spy = captureConsole('log') + await run('show') + expect(logged(spy)).toBe('Holiday (notifications paused, activity paused)') + }) + + it('shows the activity pause on its own, after the expiry', async () => { + apiMocks.getSessionUser.mockResolvedValue({ + status: { + message: 'Holiday', + activeUntil: '2026-10-20T09:00:00.000Z', + effects: { pauseActivity: { scope: { mode: 'all' } } }, + }, + }) + const spy = captureConsole('log') + await run('show') + expect(logged(spy)).toMatch(/^Holiday \(until .+, activity paused\)$/) + expect(logged(spy)).not.toContain('notifications paused') + }) + + it('keeps the JSON shape for a status without the effect', async () => { + apiMocks.getSessionUser.mockResolvedValue({ + status: { message: 'Away', effects: { pauseNotifications: {} } }, + }) + const spy = captureConsole('log') + await run('show', '--json') + expect(JSON.parse(logged(spy))).toEqual({ + status: { message: 'Away', effects: { pauseNotifications: {} } }, + }) + }) + it('says so when there is no status', async () => { apiMocks.getSessionUser.mockResolvedValue({ status: null }) const spy = captureConsole('log') @@ -105,6 +145,50 @@ describe('tdc status', () => { }) }) + it('sends only the activity effect for --pause-activity', async () => { + captureConsole('log') + await run('set', 'Holiday', '--pause-activity') + expect(users.setStatus).toHaveBeenCalledWith({ + message: 'Holiday', + effects: { pauseActivity: {} }, + }) + }) + + it('sends both effects when both flags are given', async () => { + captureConsole('log') + await run('set', 'Holiday', '--pause-notifications', '--pause-activity') + expect(users.setStatus).toHaveBeenCalledWith({ + message: 'Holiday', + effects: { pauseNotifications: {}, pauseActivity: {} }, + }) + }) + + it('lists the activity pause in the --dry-run preview only when asked for', async () => { + const spy = captureConsole('log') + await run('set', 'Holiday', '--pause-activity', '--dry-run') + expect(logged(spy)).toContain('Pause activity') + expect(users.setStatus).not.toHaveBeenCalled() + + spy.mockClear() + await run('set', 'Holiday', '--dry-run') + expect(logged(spy)).toContain('Would set status') + expect(logged(spy)).not.toContain('Pause activity') + }) + + it('prints the activity pause in the text output', async () => { + users.setStatus.mockResolvedValue({ + id: 1, + status: { + message: 'Holiday', + emoji: null, + effects: { pauseActivity: { scope: { mode: 'all' } } }, + }, + }) + const spy = captureConsole('log') + await run('set', 'Holiday', '--pause-activity') + expect(logged(spy)).toBe('Status set: Holiday (activity paused)') + }) + it('turns --for into an expiry that long from now', async () => { captureConsole('log') await run('set', 'Focus', '--for', '1h30m') @@ -341,6 +425,18 @@ describe('tdc status reports what the server returned', () => { expect(help).toContain('each app') }) + it('describes --pause-activity as protecting Karma and streaks, independent of notifications, without claiming Comms acts on it', () => { + const program = createTestProgram(registerStatusCommand) + const set = program.commands + .find((c) => c.name() === 'status') + ?.commands.find((c) => c.name() === 'set') + const help = set?.helpInformation() ?? '' + expect(help).toContain('--pause-activity') + expect(help).toContain('Karma') + expect(help).toContain('streaks') + expect(help).toMatch(/independent/i) + }) + it('succeeds, with one line, when set and clear do what was asked', async () => { const spy = captureConsole('log') await run('set', 'Away') diff --git a/src/commands/user.test.ts b/src/commands/user.test.ts index 7862720..4bf05f9 100644 --- a/src/commands/user.test.ts +++ b/src/commands/user.test.ts @@ -255,6 +255,19 @@ describe('user status line', () => { expect(lines).toContain('Status: 🎯 Deep work') }) + it('notes a paused activity on the status line', async () => { + apiMocks.getSessionUser.mockResolvedValue({ + ...baseUser, + status: { message: 'Holiday', effects: { pauseActivity: {} } }, + }) + const consoleSpy = captureConsole('log') + + await createProgram().parseAsync(['node', 'tdc', 'user']) + + const lines = consoleSpy.mock.calls.map((call) => call[0]) + expect(lines).toContain('Status: Holiday (activity paused)') + }) + it('includes the status in the curated --json output, and omits it when there is none', async () => { apiMocks.getSessionUser.mockResolvedValue({ ...baseUser, diff --git a/src/lib/skills/content.ts b/src/lib/skills/content.ts index 3c7590d..5af13af 100644 --- a/src/lib/skills/content.ts +++ b/src/lib/skills/content.ts @@ -262,6 +262,7 @@ tdc users --ids-only # Output only user IDs tdc status # Show your status (default login is enough) tdc status --json # Show it as { status } JSON (null when unset; --full for the whole user) tdc status set "Deep work" --emoji 🎯 --for 2h --pause-notifications # Set it (replaces any current status; needs --full-access) +tdc status set "On holiday" --pause-activity # Pause Todoist Karma and streaks while it is active (independent of --pause-notifications) tdc status set "On holiday" --until 2026-10-20T09:00 # End at a date (YYYY-MM-DD, YYYY-MM-DDTHH:MM or a full ISO 8601 timestamp; local time unless it has Z or an offset) tdc status set "Away" --dry-run # Preview without setting tdc status set "Away" --json # Set and return { status } as JSON @@ -328,6 +329,8 @@ All group *writes* (\`groups create\`, \`rename\`, \`delete\`, \`add-user\`, \`r Only pass \`--pause-notifications\` when the user asks for it. It holds back Comms notifications only for now: each app has to read the status and hold its own notifications back, and Todoist does not yet. Held notifications are not sent later. +Only pass \`--pause-activity\` when the user asks for it. It pauses their Todoist Karma and streaks while the status is active. It is independent of \`--pause-notifications\`: it does not hold back notifications, and pausing notifications does not protect Karma. + If a channel is not found in \`tdc channels\`, widen with broader listings such as \`tdc channels --scope public\`, then \`tdc channels --scope public --state all\`. Check \`tdc channels --help\` for other available filters. \`tdc channel threads\` returns every thread in the channel; pagination filters (\`--limit\`, \`--cursor\`, \`--since\`, \`--until\`, \`--unread\`) are applied client-side after fetch. \`--archive-filter\` is applied server-side. Results are sorted newest-first by last activity. In \`--json\` / \`--ndjson\`, the response includes a \`nextCursor\` string (opaque) you can pass via \`--cursor\` to fetch the next page; NDJSON emits the cursor as a final \`{ "_meta": true, "nextCursor": "..." }\` line. diff --git a/src/lib/status.ts b/src/lib/status.ts index d97c713..48bf25a 100644 --- a/src/lib/status.ts +++ b/src/lib/status.ts @@ -10,5 +10,8 @@ export function formatStatus(status: UserStatus): string { if (status.effects?.pauseNotifications) { parts.push('notifications paused') } + if (status.effects?.pauseActivity) { + parts.push('activity paused') + } return parts.length > 1 ? `${parts[0]} (${parts.slice(1).join(', ')})` : parts[0] } From 47e5e39563e49eba19bce5db294f86459127ab94 Mon Sep 17 00:00:00 2001 From: lmjabreu Date: Fri, 23 Oct 2026 15:48:58 +0100 Subject: [PATCH 2/2] test(status): pin pause-activity in --json and in the help wording Co-Authored-By: Claude Sonnet 5.5 --- src/commands/status/status.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/commands/status/status.test.ts b/src/commands/status/status.test.ts index baa49f5..906d8f2 100644 --- a/src/commands/status/status.test.ts +++ b/src/commands/status/status.test.ts @@ -104,6 +104,17 @@ describe('tdc status', () => { }) }) + it('passes the activity effect through --json', async () => { + const status = { + message: 'Holiday', + effects: { pauseActivity: { scope: { mode: 'all' } } }, + } + apiMocks.getSessionUser.mockResolvedValue({ status }) + const spy = captureConsole('log') + await run('show', '--json') + expect(JSON.parse(logged(spy))).toEqual({ status }) + }) + it('says so when there is no status', async () => { apiMocks.getSessionUser.mockResolvedValue({ status: null }) const spy = captureConsole('log') @@ -425,7 +436,7 @@ describe('tdc status reports what the server returned', () => { expect(help).toContain('each app') }) - it('describes --pause-activity as protecting Karma and streaks, independent of notifications, without claiming Comms acts on it', () => { + it('describes --pause-activity as protecting Karma and streaks, independent of notifications', () => { const program = createTestProgram(registerStatusCommand) const set = program.commands .find((c) => c.name() === 'status') @@ -437,6 +448,16 @@ describe('tdc status reports what the server returned', () => { expect(help).toMatch(/independent/i) }) + it('does not claim in the --pause-activity description that Comms acts on it', () => { + const program = createTestProgram(registerStatusCommand) + const set = program.commands + .find((c) => c.name() === 'status') + ?.commands.find((c) => c.name() === 'set') + const option = set?.options.find((o) => o.long === '--pause-activity') + expect(option?.description).toContain('Karma') + expect(option?.description).not.toMatch(/comms/i) + }) + it('succeeds, with one line, when set and clear do what was asked', async () => { const spy = captureConsole('log') await run('set', 'Away')