Skip to content
Open
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
3 changes: 3 additions & 0 deletions skills/comms-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion src/commands/status/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)

Expand Down
11 changes: 10 additions & 1 deletion src/commands/status/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type SetStatusOptions = MutationOptions & {
until?: string
for?: string
pauseNotifications?: boolean
pauseActivity?: boolean
}

function resolveMessage(message: string): string {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down
117 changes: 117 additions & 0 deletions src/commands/status/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,57 @@ 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('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')
Expand Down Expand Up @@ -105,6 +156,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')
Expand Down Expand Up @@ -341,6 +436,28 @@ 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', () => {
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('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')
Expand Down
13 changes: 13 additions & 0 deletions src/commands/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/skills/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/lib/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Loading