Skip to content
Merged
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
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()
})
})
16 changes: 13 additions & 3 deletions src/commands/comment/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import { formatJson, printDryRun } from '../../lib/output.js'
import { assertChannelIsPublic } from '../../lib/public-channels.js'
import { resolveCommentId } from '../../lib/refs.js'

type DeleteOptions = MutationOptions

export async function deleteComment(ref: string, options: DeleteOptions): Promise<void> {
export async function deleteComment(ref: string, options: MutationOptions): Promise<void> {
const commentId = resolveCommentId(ref)

if (!options.yes && !options.dryRun) {
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
}

const client = await getCommsClient()
const [comment, user] = await Promise.all([
client.comments.getComment(commentId),
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
15 changes: 14 additions & 1 deletion 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 @@ -7,16 +8,28 @@ export async function markConversationDone(ref: string, options: DoneOptions): P
const conversationId = resolveConversationId(ref)

const client = await getCommsClient()
const conversation = await client.conversations.getConversation(conversationId)

if (options.dryRun) {
const conversation = await client.conversations.getConversation(conversationId)
printDryRun('archive conversation', {
Conversation: conversationLabel(conversation),
Status: conversation.archived ? 'already archived' : undefined,
})
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.',
)
}
console.log(`Would archive: ${conversationLabel(conversation)}`)
console.log('Use --yes to confirm.')
return
}

await client.conversations.archiveConversation(conversationId)

if (options.json) {
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
16 changes: 13 additions & 3 deletions src/commands/msg/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@ import type { MutationOptions } from '../../lib/options.js'
import { formatJson, printDryRun } from '../../lib/output.js'
import { resolveMessageId } from '../../lib/refs.js'

type DeleteOptions = MutationOptions

export async function deleteMessage(ref: string, options: DeleteOptions): Promise<void> {
export async function deleteMessage(ref: string, options: MutationOptions): Promise<void> {
const messageId = resolveMessageId(ref)

if (!options.yes && !options.dryRun) {
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
}

const client = await getCommsClient()
const [message, user] = await Promise.all([
client.conversationMessages.getMessage(messageId),
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