diff --git a/docs/SPEC.md b/docs/SPEC.md index c8ea4a3..c045903 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -205,6 +205,7 @@ Arguments: Options: +- `--yes` - Confirm archive (required to execute) - `--dry-run` - Show what would happen without executing --- @@ -276,6 +277,7 @@ Arguments: Options: +- `--yes` - Confirm archive (required to execute) - `--dry-run` - Show what would happen without executing --- @@ -322,6 +324,7 @@ Arguments: Options: +- `--yes` - Confirm deletion (required to execute) - `--dry-run` - Show what would happen without executing --- @@ -499,7 +502,7 @@ echo "Multiline\nreply" | tw thread reply id:123456 tw thread reply id:123456 # opens $EDITOR # Mark thread as done -tw thread done id:123456 +tw thread done id:123456 --yes # List unread conversations tw conversation unread diff --git a/skills/twist-cli/SKILL.md b/skills/twist-cli/SKILL.md index 407e946..6a355e1 100644 --- a/skills/twist-cli/SKILL.md +++ b/skills/twist-cli/SKILL.md @@ -82,8 +82,9 @@ tw thread reply "content" --json # Post and return comment as JSON tw thread reply "content" --json --full # Include all comment fields tw thread reply "content" --close # Reply and close the thread tw thread reply "content" --reopen # Reply and reopen a closed thread -tw thread done # Archive thread (mark done) -tw thread done --json # Archive and return status as JSON +tw thread done # Preview thread archive (requires --yes to execute) +tw thread done --yes # Archive thread (mark done) +tw thread done --yes --json # Archive and return status as JSON tw thread mute # Mute thread for 60 minutes (default) tw thread mute --minutes 480 # Mute for custom duration tw thread mute --json # Mute and return { id, mutedUntil } as JSON @@ -119,8 +120,9 @@ tw comment view --json --full # Include all fields in JSON outp tw comment update "new content" # Update a thread comment tw comment update "content" --json # Update and return updated comment as JSON tw comment update "content" --json --full # Include all comment fields -tw comment delete # Delete a thread comment -tw comment delete --json # Delete and return status as JSON +tw comment delete # Preview comment deletion (requires --yes to execute) +tw comment delete --yes # Delete a thread comment +tw comment delete --yes --json # Delete and return status as JSON ``` ## Conversations (DMs/Groups) @@ -135,8 +137,9 @@ tw conversation with --include-groups # List any conversations with t tw conversation reply "content" # Send a message tw conversation reply "content" --json # Send and return message as JSON tw conversation reply "content" --json --full # Include all message fields -tw conversation done # Archive conversation -tw conversation done --json # Archive and return status as JSON +tw conversation done # Preview conversation archive (requires --yes to execute) +tw conversation done --yes # Archive conversation +tw conversation done --yes --json # Archive and return status as JSON tw conversation mute # Mute conversation for 60 minutes (default) tw conversation mute --minutes 480 # Mute for custom duration tw conversation mute --json # Mute and return { id, mutedUntil } as JSON @@ -155,8 +158,9 @@ tw msg view # View a single conversation message tw msg update "content" # Edit a conversation message tw msg update "content" --json # Edit and return updated message as JSON tw msg update "content" --json --full # Include all message fields -tw msg delete # Delete a conversation message -tw msg delete --json # Delete and return status as JSON +tw msg delete # Preview message deletion (requires --yes to execute) +tw msg delete --yes # Delete a conversation message +tw msg delete --yes --json # Delete and return status as JSON ``` Alias: `tw message` works the same as `tw msg`. @@ -321,7 +325,7 @@ tw view https://twist.com/a/1585/msg/400/m/500 --json # View message as JSON tw inbox --unread --json tw thread view --unread tw thread reply "Thanks, I'll look into this." -tw thread done +tw thread done --yes ``` **Search and review:** diff --git a/src/commands/comment/comment.test.ts b/src/commands/comment/comment.test.ts index 5039605..1be3289 100644 --- a/src/commands/comment/comment.test.ts +++ b/src/commands/comment/comment.test.ts @@ -254,19 +254,33 @@ describe('comment delete', () => { vi.clearAllMocks() }) - it('deletes a comment', async () => { + it('deletes a comment with --yes', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) const program = createProgram() const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - await program.parseAsync(['node', 'tw', 'comment', 'delete', '300']) + await program.parseAsync(['node', 'tw', 'comment', 'delete', '300', '--yes']) expect(client.comments.deleteComment).toHaveBeenCalledWith(300) expect(consoleSpy).toHaveBeenCalledWith('Comment 300 deleted.') consoleSpy.mockRestore() }) + it('prompts for confirmation without --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'comment', 'delete', '300']) + + expect(consoleSpy).toHaveBeenCalledWith('Would delete comment 300') + expect(consoleSpy).toHaveBeenCalledWith('Use --yes to confirm.') + expect(client.comments.deleteComment).not.toHaveBeenCalled() + consoleSpy.mockRestore() + }) + it('shows dry run output', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) @@ -307,16 +321,28 @@ 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.getTwistClient.mockResolvedValue(client) const program = createProgram() const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - await program.parseAsync(['node', 'tw', 'comment', 'delete', '300', '--json']) + await program.parseAsync(['node', 'tw', 'comment', 'delete', '300', '--json', '--yes']) const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0]) expect(jsonOutput).toEqual({ id: 300, deleted: true }) consoleSpy.mockRestore() }) + + it('errors when --json is used without --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + const program = createProgram() + + await expect( + program.parseAsync(['node', 'tw', 'comment', 'delete', '300', '--json']), + ).rejects.toHaveProperty('code', 'MISSING_YES_FLAG') + + expect(client.comments.deleteComment).not.toHaveBeenCalled() + }) }) diff --git a/src/commands/comment/delete.ts b/src/commands/comment/delete.ts index c21088d..3c5198d 100644 --- a/src/commands/comment/delete.ts +++ b/src/commands/comment/delete.ts @@ -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 { const commentId = resolveCommentId(ref) @@ -36,6 +36,18 @@ export async function deleteComment(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 comment ${commentId}`) + console.log('Use --yes to confirm.') + return + } + await client.comments.deleteComment(commentId) if (options.json) { diff --git a/src/commands/comment/index.ts b/src/commands/comment/index.ts index 6bb7511..23afd2f 100644 --- a/src/commands/comment/index.ts +++ b/src/commands/comment/index.ts @@ -49,13 +49,14 @@ Examples: comment .command('delete ') .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: - tw comment delete 12345 + tw comment delete 12345 --yes tw comment delete 12345 --dry-run`, ) .action(deleteComment) diff --git a/src/commands/conversation/conversation.test.ts b/src/commands/conversation/conversation.test.ts index bdb4df9..c33b8db 100644 --- a/src/commands/conversation/conversation.test.ts +++ b/src/commands/conversation/conversation.test.ts @@ -679,7 +679,7 @@ 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.getTwistClient.mockResolvedValue(client) @@ -687,7 +687,7 @@ describe('conversation done', () => { const program = createProgram() const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - await program.parseAsync(['node', 'tw', 'conversation', 'done', '42']) + await program.parseAsync(['node', 'tw', 'conversation', 'done', '42', '--yes']) expect(client.conversations.archiveConversation).toHaveBeenCalledWith(42) expect(consoleSpy).toHaveBeenCalledWith('Conversation 42 archived.') @@ -695,6 +695,52 @@ describe('conversation done', () => { consoleSpy.mockRestore() }) + 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.getTwistClient.mockResolvedValue(client) + + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'conversation', 'done', '42']) + + expect(consoleSpy).toHaveBeenCalledWith('Would archive: conversation 42') + expect(consoleSpy).toHaveBeenCalledWith('Use --yes to confirm.') + expect(client.conversations.archiveConversation).not.toHaveBeenCalled() + + consoleSpy.mockRestore() + }) + + 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.getTwistClient.mockResolvedValue(client) + + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'conversation', 'done', '42', '--json', '--yes']) + + const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0]) + expect(jsonOutput).toEqual({ id: 42, archived: true }) + + consoleSpy.mockRestore() + }) + + 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.getTwistClient.mockResolvedValue(client) + const program = createProgram() + + await expect( + program.parseAsync(['node', 'tw', '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] }) diff --git a/src/commands/conversation/done.ts b/src/commands/conversation/done.ts index 05197ce..9da27e1 100644 --- a/src/commands/conversation/done.ts +++ b/src/commands/conversation/done.ts @@ -1,4 +1,5 @@ import { getTwistClient } 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' @@ -17,6 +18,19 @@ export async function markConversationDone(ref: string, options: DoneOptions): P return } + if (!options.yes) { + 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) { diff --git a/src/commands/conversation/helpers.ts b/src/commands/conversation/helpers.ts index b6aafbb..00c4633 100644 --- a/src/commands/conversation/helpers.ts +++ b/src/commands/conversation/helpers.ts @@ -22,7 +22,7 @@ export type ReplyOptions = MutationOptions export type MuteOptions = MutationOptions & { minutes?: string } -export type DoneOptions = MutationOptions +export type DoneOptions = MutationOptions & { yes?: boolean } export const CONVERSATION_PAGE_LIMIT = 100 diff --git a/src/commands/conversation/index.ts b/src/commands/conversation/index.ts index 62510cd..d877783 100644 --- a/src/commands/conversation/index.ts +++ b/src/commands/conversation/index.ts @@ -93,13 +93,14 @@ Examples: conversation .command('done ') .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: - tw conversation done 12345 + tw conversation done 12345 --yes tw conversation done 12345 --dry-run`, ) .action(markConversationDone) diff --git a/src/commands/msg/delete.ts b/src/commands/msg/delete.ts index c6e4a73..926d33a 100644 --- a/src/commands/msg/delete.ts +++ b/src/commands/msg/delete.ts @@ -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 { const messageId = resolveMessageId(ref) @@ -33,6 +33,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) { diff --git a/src/commands/msg/index.ts b/src/commands/msg/index.ts index a0b31d1..776492d 100644 --- a/src/commands/msg/index.ts +++ b/src/commands/msg/index.ts @@ -47,13 +47,14 @@ Examples: msg.command('delete ') .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: - tw msg delete 12345 + tw msg delete 12345 --yes tw msg delete 12345 --dry-run`, ) .action(deleteMessage) diff --git a/src/commands/msg/msg.test.ts b/src/commands/msg/msg.test.ts index 3bc8fec..c77d60b 100644 --- a/src/commands/msg/msg.test.ts +++ b/src/commands/msg/msg.test.ts @@ -102,19 +102,33 @@ describe('msg delete', () => { vi.clearAllMocks() }) - it('deletes a message', async () => { + it('deletes a message with --yes', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) const program = createProgram() const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - await program.parseAsync(['node', 'tw', 'msg', 'delete', '200']) + await program.parseAsync(['node', 'tw', 'msg', 'delete', '200', '--yes']) expect(client.conversationMessages.deleteMessage).toHaveBeenCalledWith(200) expect(consoleSpy).toHaveBeenCalledWith('Message 200 deleted.') consoleSpy.mockRestore() }) + it('prompts for confirmation without --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'msg', 'delete', '200']) + + expect(consoleSpy).toHaveBeenCalledWith('Would delete message 200') + expect(consoleSpy).toHaveBeenCalledWith('Use --yes to confirm.') + expect(client.conversationMessages.deleteMessage).not.toHaveBeenCalled() + consoleSpy.mockRestore() + }) + it('shows dry run output', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) @@ -141,16 +155,28 @@ describe('msg delete', () => { expect(client.conversationMessages.deleteMessage).not.toHaveBeenCalled() }) - it('outputs JSON with --json', async () => { + it('outputs JSON with --json --yes', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) const program = createProgram() const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - await program.parseAsync(['node', 'tw', 'msg', 'delete', '200', '--json']) + await program.parseAsync(['node', 'tw', 'msg', 'delete', '200', '--json', '--yes']) const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0]) expect(jsonOutput).toEqual({ id: 200, deleted: true }) consoleSpy.mockRestore() }) + + it('errors when --json is used without --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + const program = createProgram() + + await expect( + program.parseAsync(['node', 'tw', 'msg', 'delete', '200', '--json']), + ).rejects.toHaveProperty('code', 'MISSING_YES_FLAG') + + expect(client.conversationMessages.deleteMessage).not.toHaveBeenCalled() + }) }) diff --git a/src/commands/thread/index.ts b/src/commands/thread/index.ts index f1c40f1..c52be8f 100644 --- a/src/commands/thread/index.ts +++ b/src/commands/thread/index.ts @@ -88,15 +88,16 @@ Examples: thread .command('done ') .description('Archive a thread (mark as done)') + .option('--yes', 'Confirm archive') .option('--dry-run', 'Show what would happen without executing') .option('--json', 'Output result as JSON') .addHelpText( 'after', ` Examples: - tw thread done 12345 + tw thread done 12345 --yes tw thread done 12345 --dry-run - tw thread done 12345 --json`, + tw thread done 12345 --json --yes`, ) .action(markThreadDone) diff --git a/src/commands/thread/mutate.ts b/src/commands/thread/mutate.ts index 3e17853..d5028e1 100644 --- a/src/commands/thread/mutate.ts +++ b/src/commands/thread/mutate.ts @@ -1,10 +1,11 @@ import { getTwistClient } from '../../lib/api.js' +import { CliError } from '../../lib/errors.js' import type { MutationOptions } from '../../lib/options.js' import { formatJson, printDryRun } from '../../lib/output.js' import { assertChannelIsPublic } from '../../lib/public-channels.js' import { resolveThreadId } from '../../lib/refs.js' -type DoneOptions = MutationOptions +type DoneOptions = MutationOptions & { yes?: boolean } export async function markThreadDone(ref: string, options: DoneOptions): Promise { const threadId = resolveThreadId(ref) @@ -20,6 +21,18 @@ export async function markThreadDone(ref: string, options: DoneOptions): Promise return } + if (!options.yes) { + if (options.json) { + throw new CliError( + 'MISSING_YES_FLAG', + '--yes is required to execute archive in --json mode.', + ) + } + console.log(`Would archive: ${thread.title}`) + console.log('Use --yes to confirm.') + return + } + await client.inbox.archiveThread(threadId) if (options.json) { diff --git a/src/commands/thread/thread.test.ts b/src/commands/thread/thread.test.ts index 27ac41e..bd94c1d 100644 --- a/src/commands/thread/thread.test.ts +++ b/src/commands/thread/thread.test.ts @@ -1136,14 +1136,14 @@ describe('thread done', () => { vi.clearAllMocks() }) - it('archives a thread', async () => { + it('archives a thread with --yes', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) const program = createProgram() const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - await program.parseAsync(['node', 'tw', 'thread', 'done', '500']) + await program.parseAsync(['node', 'tw', 'thread', 'done', '500', '--yes']) expect(client.inbox.archiveThread).toHaveBeenCalledWith(500) expect(consoleSpy).toHaveBeenCalledWith('Thread 500 archived.') @@ -1151,6 +1151,49 @@ describe('thread done', () => { consoleSpy.mockRestore() }) + it('prompts for confirmation without --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'thread', 'done', '500']) + + expect(consoleSpy).toHaveBeenCalledWith('Would archive: Test Thread') + expect(consoleSpy).toHaveBeenCalledWith('Use --yes to confirm.') + expect(client.inbox.archiveThread).not.toHaveBeenCalled() + + consoleSpy.mockRestore() + }) + + it('outputs JSON with --json --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'thread', 'done', '500', '--json', '--yes']) + + const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0]) + expect(jsonOutput).toEqual({ id: 500, isArchived: true }) + + consoleSpy.mockRestore() + }) + + it('errors when --json is used without --yes', async () => { + const client = createClient() + apiMocks.getTwistClient.mockResolvedValue(client) + const program = createProgram() + + await expect( + program.parseAsync(['node', 'tw', 'thread', 'done', '500', '--json']), + ).rejects.toHaveProperty('code', 'MISSING_YES_FLAG') + + expect(client.inbox.archiveThread).not.toHaveBeenCalled() + }) + it('shows dry run output', async () => { const client = createClient() apiMocks.getTwistClient.mockResolvedValue(client) diff --git a/src/lib/skills/content.ts b/src/lib/skills/content.ts index f3bf85d..a50bb22 100644 --- a/src/lib/skills/content.ts +++ b/src/lib/skills/content.ts @@ -86,8 +86,9 @@ tw thread reply "content" --json # Post and return comment as JSON tw thread reply "content" --json --full # Include all comment fields tw thread reply "content" --close # Reply and close the thread tw thread reply "content" --reopen # Reply and reopen a closed thread -tw thread done # Archive thread (mark done) -tw thread done --json # Archive and return status as JSON +tw thread done # Preview thread archive (requires --yes to execute) +tw thread done --yes # Archive thread (mark done) +tw thread done --yes --json # Archive and return status as JSON tw thread mute # Mute thread for 60 minutes (default) tw thread mute --minutes 480 # Mute for custom duration tw thread mute --json # Mute and return { id, mutedUntil } as JSON @@ -123,8 +124,9 @@ tw comment view --json --full # Include all fields in JSON outp tw comment update "new content" # Update a thread comment tw comment update "content" --json # Update and return updated comment as JSON tw comment update "content" --json --full # Include all comment fields -tw comment delete # Delete a thread comment -tw comment delete --json # Delete and return status as JSON +tw comment delete # Preview comment deletion (requires --yes to execute) +tw comment delete --yes # Delete a thread comment +tw comment delete --yes --json # Delete and return status as JSON \`\`\` ## Conversations (DMs/Groups) @@ -139,8 +141,9 @@ tw conversation with --include-groups # List any conversations with t tw conversation reply "content" # Send a message tw conversation reply "content" --json # Send and return message as JSON tw conversation reply "content" --json --full # Include all message fields -tw conversation done # Archive conversation -tw conversation done --json # Archive and return status as JSON +tw conversation done # Preview conversation archive (requires --yes to execute) +tw conversation done --yes # Archive conversation +tw conversation done --yes --json # Archive and return status as JSON tw conversation mute # Mute conversation for 60 minutes (default) tw conversation mute --minutes 480 # Mute for custom duration tw conversation mute --json # Mute and return { id, mutedUntil } as JSON @@ -159,8 +162,9 @@ tw msg view # View a single conversation message tw msg update "content" # Edit a conversation message tw msg update "content" --json # Edit and return updated message as JSON tw msg update "content" --json --full # Include all message fields -tw msg delete # Delete a conversation message -tw msg delete --json # Delete and return status as JSON +tw msg delete # Preview message deletion (requires --yes to execute) +tw msg delete --yes # Delete a conversation message +tw msg delete --yes --json # Delete and return status as JSON \`\`\` Alias: \`tw message\` works the same as \`tw msg\`. @@ -325,7 +329,7 @@ tw view https://twist.com/a/1585/msg/400/m/500 --json # View message as JSON tw inbox --unread --json tw thread view --unread tw thread reply "Thanks, I'll look into this." -tw thread done +tw thread done --yes \`\`\` **Search and review:**