Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Arguments:

Options:

- `--yes` - Confirm archive (required to execute)
- `--dry-run` - Show what would happen without executing

---
Expand Down Expand Up @@ -275,6 +276,7 @@ Arguments:

Options:

- `--yes` - Confirm archive (required to execute)
- `--dry-run` - Show what would happen without executing

---
Expand Down Expand Up @@ -321,6 +323,7 @@ Arguments:

Options:

- `--yes` - Confirm deletion (required to execute)
- `--dry-run` - Show what would happen without executing

---
Expand Down Expand Up @@ -498,7 +501,7 @@ echo "Multiline\nreply" | tdc thread reply id:123456
tdc thread reply id:123456 # opens $EDITOR

# Mark thread as done
tdc thread done id:123456
tdc thread done id:123456 --yes

# List unread conversations
tdc conversation unread
Expand Down
22 changes: 13 additions & 9 deletions skills/comms-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ tdc thread reply <ref> "content" --json # Post and return comment as JSON
tdc thread reply <ref> "content" --json --full # Include all comment fields
tdc thread reply <ref> "content" --close # Reply and close the thread
tdc thread reply <ref> "content" --reopen # Reply and reopen a closed thread
tdc thread done <ref> # Archive thread (mark done)
tdc thread done <ref> --json # Archive and return status as JSON
tdc thread done <ref> # Preview thread archive (requires --yes to execute)
tdc thread done <ref> --yes # Archive thread (mark done)
tdc thread done <ref> --yes --json # Archive and return status as JSON
tdc thread mute <ref> # Mute thread for 60 minutes (default)
tdc thread mute <ref> --minutes 480 # Mute for custom duration
tdc thread mute <ref> --json # Mute and return { id, mutedUntil } as JSON
Expand Down Expand Up @@ -135,8 +136,9 @@ tdc comment view <comment-ref> --json --full # Include all fields in JSON out
tdc comment update <comment-ref> "new content" # Update a thread comment
tdc comment update <comment-ref> "content" --json # Update and return updated comment as JSON
tdc comment update <comment-ref> "content" --json --full # Include all comment fields
tdc comment delete <comment-ref> # Delete a thread comment
tdc comment delete <comment-ref> --json # Delete and return status as JSON
tdc comment delete <comment-ref> # Preview comment deletion (requires --yes to execute)
tdc comment delete <comment-ref> --yes # Delete a thread comment
tdc comment delete <comment-ref> --yes --json # Delete and return status as JSON
```

## Conversations (DMs/Groups)
Expand All @@ -151,8 +153,9 @@ tdc conversation with <user-ref> --include-groups # List any conversations with
tdc conversation reply <ref> "content" # Send a message
tdc conversation reply <ref> "content" --json # Send and return message as JSON
tdc conversation reply <ref> "content" --json --full # Include all message fields
tdc conversation done <ref> # Archive conversation
tdc conversation done <ref> --json # Archive and return status as JSON
tdc conversation done <ref> # Preview conversation archive (requires --yes to execute)
tdc conversation done <ref> --yes # Archive conversation
tdc conversation done <ref> --yes --json # Archive and return status as JSON
tdc conversation mute <ref> # Mute conversation for 60 minutes (default)
tdc conversation mute <ref> --minutes 480 # Mute for custom duration
tdc conversation mute <ref> --json # Mute and return { id, mutedUntil } as JSON
Expand All @@ -171,8 +174,9 @@ tdc msg view <message-ref> # View a single conversation message
tdc msg update <ref> "content" # Edit a conversation message
tdc msg update <ref> "content" --json # Edit and return updated message as JSON
tdc msg update <ref> "content" --json --full # Include all message fields
tdc msg delete <ref> # Delete a conversation message
tdc msg delete <ref> --json # Delete and return status as JSON
tdc msg delete <ref> # Preview message deletion (requires --yes to execute)
tdc msg delete <ref> --yes # Delete a conversation message
tdc msg delete <ref> --yes --json # Delete and return status as JSON
```

Alias: `tdc message` works the same as `tdc msg`.
Expand Down Expand Up @@ -395,7 +399,7 @@ tdc view https://comms.todoist.com/a/1585/msg/400/m/500 --json # View message
tdc inbox --unread --json
tdc thread view <id> --unread
tdc thread reply <id> "Thanks, I'll look into this."
tdc thread done <id>
tdc thread done <id> --yes
```

**Search and review:**
Expand Down
33 changes: 29 additions & 4 deletions src/commands/comment/comment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,18 +220,31 @@ describe('comment delete', () => {
vi.clearAllMocks()
})

it('deletes a comment', async () => {
it('deletes a comment with --yes', async () => {
const client = createClient()
apiMocks.getCommsClient.mockResolvedValue(client)
const program = createProgram()
const consoleSpy = captureConsole('log')

await program.parseAsync(['node', 'tdc', 'comment', 'delete', '300'])
await program.parseAsync(['node', 'tdc', 'comment', 'delete', '300', '--yes'])

expect(client.comments.deleteComment).toHaveBeenCalledWith('300')
expect(consoleSpy).toHaveBeenCalledWith('Comment 300 deleted.')
})

it('prompts for confirmation without --yes', async () => {
const client = createClient()
apiMocks.getCommsClient.mockResolvedValue(client)
const program = createProgram()
const consoleSpy = captureConsole('log')

await program.parseAsync(['node', 'tdc', 'comment', 'delete', '300'])

expect(consoleSpy).toHaveBeenCalledWith('Would delete comment 300')
expect(consoleSpy).toHaveBeenCalledWith('Use --yes to confirm.')
expect(client.comments.deleteComment).not.toHaveBeenCalled()
})

it('shows dry run output', async () => {
const client = createClient()
apiMocks.getCommsClient.mockResolvedValue(client)
Expand Down Expand Up @@ -271,15 +284,27 @@ describe('comment delete', () => {
expect(client.comments.deleteComment).not.toHaveBeenCalled()
})

it('outputs JSON with --json', async () => {
it('outputs JSON with --json --yes', async () => {
const client = createClient()
apiMocks.getCommsClient.mockResolvedValue(client)
const program = createProgram()
const consoleSpy = captureConsole('log')

await program.parseAsync(['node', 'tdc', 'comment', 'delete', '300', '--json'])
await program.parseAsync(['node', 'tdc', 'comment', 'delete', '300', '--json', '--yes'])

const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0])
expect(jsonOutput).toEqual({ id: '300', deleted: true })
})

it('errors when --json is used without --yes', async () => {
const client = createClient()
apiMocks.getCommsClient.mockResolvedValue(client)
const program = createProgram()

await expect(
program.parseAsync(['node', 'tdc', 'comment', 'delete', '300', '--json']),
).rejects.toHaveProperty('code', 'MISSING_YES_FLAG')

expect(client.comments.deleteComment).not.toHaveBeenCalled()
})
})
14 changes: 13 additions & 1 deletion src/commands/comment/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { formatJson, printDryRun } from '../../lib/output.js'
import { assertChannelIsPublic } from '../../lib/public-channels.js'
import { resolveCommentId } from '../../lib/refs.js'

type DeleteOptions = MutationOptions
type DeleteOptions = MutationOptions & { yes?: boolean }

export async function deleteComment(ref: string, options: DeleteOptions): Promise<void> {
const commentId = resolveCommentId(ref)
Expand Down Expand Up @@ -33,6 +33,18 @@ export async function deleteComment(ref: string, options: DeleteOptions): Promis
return
}

if (!options.yes) {
Comment thread
scottlovegrove marked this conversation as resolved.
Outdated
if (options.json) {
throw new CliError(
'MISSING_YES_FLAG',
'--yes is required to execute deletion in --json mode.',
)
}
console.log(`Would delete comment ${commentId}`)
console.log('Use --yes to confirm.')
return
}

await client.comments.deleteComment(commentId)

if (options.json) {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/comment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ Examples:
comment
.command('delete <comment-ref>')
.description('Delete a thread comment')
.option('--yes', 'Confirm deletion')
.option('--dry-run', 'Show what would happen without executing')
.option('--json', 'Output result as JSON')
.addHelpText(
'after',
`
Examples:
tdc comment delete 12345
tdc comment delete 12345 --yes
tdc comment delete 12345 --dry-run`,
)
.action(deleteComment)
Expand Down
46 changes: 44 additions & 2 deletions src/commands/conversation/conversation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,20 +601,62 @@ describe('conversation done', () => {
vi.clearAllMocks()
})

it('archives a conversation', async () => {
it('archives a conversation with --yes', async () => {
const conversation = createConversation(42, [1, 2], '2026-03-08T10:00:00.000Z')
const client = createClient({ activeConversations: [conversation] })
apiMocks.getCommsClient.mockResolvedValue(client)

const program = createProgram()
const consoleSpy = captureConsole('log')

await program.parseAsync(['node', 'tdc', 'conversation', 'done', '42'])
await program.parseAsync(['node', 'tdc', 'conversation', 'done', '42', '--yes'])

expect(client.conversations.archiveConversation).toHaveBeenCalledWith('42')
expect(consoleSpy).toHaveBeenCalledWith('Conversation 42 archived.')
})

it('prompts for confirmation without --yes', async () => {
const conversation = createConversation(42, [1, 2], '2026-03-08T10:00:00.000Z')
const client = createClient({ activeConversations: [conversation] })
apiMocks.getCommsClient.mockResolvedValue(client)

const program = createProgram()
const consoleSpy = captureConsole('log')

await program.parseAsync(['node', 'tdc', 'conversation', 'done', '42'])

expect(consoleSpy).toHaveBeenCalledWith('Would archive: conversation 42')
expect(consoleSpy).toHaveBeenCalledWith('Use --yes to confirm.')
expect(client.conversations.archiveConversation).not.toHaveBeenCalled()
})

it('outputs JSON with --json --yes', async () => {
const conversation = createConversation(42, [1, 2], '2026-03-08T10:00:00.000Z')
const client = createClient({ activeConversations: [conversation] })
apiMocks.getCommsClient.mockResolvedValue(client)

const program = createProgram()
const consoleSpy = captureConsole('log')

await program.parseAsync(['node', 'tdc', 'conversation', 'done', '42', '--json', '--yes'])

const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0])
expect(jsonOutput).toEqual({ id: '42', archived: true })
})

it('errors when --json is used without --yes', async () => {
const conversation = createConversation(42, [1, 2], '2026-03-08T10:00:00.000Z')
const client = createClient({ activeConversations: [conversation] })
apiMocks.getCommsClient.mockResolvedValue(client)
const program = createProgram()

await expect(
program.parseAsync(['node', 'tdc', 'conversation', 'done', '42', '--json']),
).rejects.toHaveProperty('code', 'MISSING_YES_FLAG')

expect(client.conversations.archiveConversation).not.toHaveBeenCalled()
})

it('shows dry run output', async () => {
const conversation = createConversation(42, [1, 2], '2026-03-08T10:00:00.000Z')
const client = createClient({ activeConversations: [conversation] })
Expand Down
14 changes: 14 additions & 0 deletions src/commands/conversation/done.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getCommsClient } from '../../lib/api.js'
import { CliError } from '../../lib/errors.js'
import { formatJson, printDryRun } from '../../lib/output.js'
import { resolveConversationId } from '../../lib/refs.js'
import { conversationLabel, type DoneOptions } from './helpers.js'
Expand All @@ -17,6 +18,19 @@ export async function markConversationDone(ref: string, options: DoneOptions): P
return
}

if (!options.yes) {
Comment thread
scottlovegrove marked this conversation as resolved.
if (options.json) {
throw new CliError(
'MISSING_YES_FLAG',
'--yes is required to execute archive in --json mode.',
)
}
const conversation = await client.conversations.getConversation(conversationId)
console.log(`Would archive: ${conversationLabel(conversation)}`)
console.log('Use --yes to confirm.')
return
}

await client.conversations.archiveConversation(conversationId)

if (options.json) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/conversation/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type ReplyOptions = MutationOptions

export type MuteOptions = MutationOptions & { minutes?: string }

export type DoneOptions = MutationOptions
export type DoneOptions = MutationOptions & { yes?: boolean }
Comment thread
scottlovegrove marked this conversation as resolved.
Outdated

export type ConversationLookupResult = {
directConversation?: Conversation
Expand Down
3 changes: 2 additions & 1 deletion src/commands/conversation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,14 @@ Examples:
conversation
.command('done <conversation-ref>')
.description('Archive a conversation')
.option('--yes', 'Confirm archive')
.option('--dry-run', 'Show what would happen without executing')
.option('--json', 'Output result as JSON')
.addHelpText(
'after',
`
Examples:
tdc conversation done 12345
tdc conversation done 12345 --yes
tdc conversation done 12345 --dry-run`,
)
.action(markConversationDone)
Expand Down
14 changes: 13 additions & 1 deletion src/commands/msg/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MutationOptions } from '../../lib/options.js'
import { formatJson, printDryRun } from '../../lib/output.js'
import { resolveMessageId } from '../../lib/refs.js'

type DeleteOptions = MutationOptions
type DeleteOptions = MutationOptions & { yes?: boolean }

export async function deleteMessage(ref: string, options: DeleteOptions): Promise<void> {
const messageId = resolveMessageId(ref)
Expand All @@ -30,6 +30,18 @@ export async function deleteMessage(ref: string, options: DeleteOptions): Promis
return
}

if (!options.yes) {
if (options.json) {
throw new CliError(
'MISSING_YES_FLAG',
'--yes is required to execute deletion in --json mode.',
)
}
console.log(`Would delete message ${messageId}`)
console.log('Use --yes to confirm.')
return
}

await client.conversationMessages.deleteMessage(messageId)

if (options.json) {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/msg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ Examples:

msg.command('delete <message-ref>')
.description('Delete a conversation message')
.option('--yes', 'Confirm deletion')
.option('--dry-run', 'Show what would happen without executing')
.option('--json', 'Output result as JSON')
.addHelpText(
'after',
`
Examples:
tdc msg delete 12345
tdc msg delete 12345 --yes
tdc msg delete 12345 --dry-run`,
)
.action(deleteMessage)
Expand Down
Loading
Loading