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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ This is a TypeScript CLI (`tw`) for Twist messaging, built with Commander.js.

- **Implicit view subcommand**: `tw thread <ref>` defaults to `tw thread view <ref>` via Commander's `{ isDefault: true }`. Same for `conversation` and `msg`. Edge case: if a ref matches a subcommand name (e.g., "reply"), the subcommand wins — user must use `tw thread view reply`
- **Named flag aliases**: Where commands accept positional `[workspace-ref]`, the `--workspace` flag is also accepted. Error if both positional and flag are provided
- **JSON output on mutating commands**: Mutating commands (create, update, delete, archive) should support `--json` output where it provides scripting value. Commands that return an object from the API (create/update) should also support `--full`. Commands where the API returns void should output a minimal status object (e.g. `{ id, deleted: true }` or `{ id, isArchived: true }`). Extend `MutationOptions` in `src/lib/options.ts` (which already includes `json` and `full`) rather than adding these fields ad hoc. Use `formatJson()` from `src/lib/output.ts` for the output. See `src/commands/away.ts` as the reference implementation.

## Pre-commit Hooks

Expand Down
14 changes: 14 additions & 0 deletions src/commands/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@ async function replyToConversation(
content: replyContent,
})

if (options.json) {
console.log(formatJson(message, 'message', options.full))
return
}

console.log(`Message sent: ${message.url}`)
}

Expand All @@ -413,6 +418,12 @@ async function markConversationDone(ref: string, options: DoneOptions): Promise<

const client = await getTwistClient()
await client.conversations.archiveConversation(conversationId)

if (options.json) {
console.log(formatJson({ id: conversationId, archived: true }))
return
}

console.log(`Conversation ${conversationId} archived.`)
}

Expand Down Expand Up @@ -529,11 +540,14 @@ export function registerConversationCommand(program: Command): void {
.command('reply <conversation-ref> [content]')
.description('Send a message in a conversation')
.option('--dry-run', 'Show what would be sent without sending')
.option('--json', 'Output sent message as JSON')
.option('--full', 'Include all fields in JSON output')
.action(replyToConversation)

conversation
.command('done <conversation-ref>')
.description('Archive a conversation')
.option('--dry-run', 'Show what would happen without executing')
.option('--json', 'Output result as JSON')
.action(markConversationDone)
}
14 changes: 14 additions & 0 deletions src/commands/msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ async function updateMessage(
content: newContent,
})

if (options.json) {
console.log(formatJson(message, 'message', options.full))
return
}

console.log(`Message updated: ${message.url}`)
}

Expand All @@ -86,6 +91,12 @@ async function deleteMessage(ref: string, options: DeleteOptions): Promise<void>

const client = await getTwistClient()
await client.conversationMessages.deleteMessage(messageId)

if (options.json) {
console.log(formatJson({ id: messageId, deleted: true }))
return
}

console.log(`Message ${messageId} deleted.`)
}

Expand All @@ -112,10 +123,13 @@ export function registerMsgCommand(program: Command): void {
msg.command('update <message-ref> [content]')
.description('Edit a conversation message')
.option('--dry-run', 'Show what would be updated without updating')
.option('--json', 'Output updated message as JSON')
.option('--full', 'Include all fields in JSON output')
.action(updateMessage)

msg.command('delete <message-ref>')
.description('Delete a conversation message')
.option('--dry-run', 'Show what would happen without executing')
.option('--json', 'Output result as JSON')
.action(deleteMessage)
}
3 changes: 3 additions & 0 deletions src/commands/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ export function registerThreadCommand(program: Command): void {
),
)
.option('--dry-run', 'Show what would be posted without posting')
.option('--json', 'Output posted comment as JSON')
.option('--full', 'Include all fields in JSON output')
.action(replyToThread)

thread
.command('done <thread-ref>')
.description('Archive a thread (mark as done)')
.option('--dry-run', 'Show what would happen without executing')
.option('--json', 'Output result as JSON')
.action(markThreadDone)
}
7 changes: 7 additions & 0 deletions src/commands/thread/mutate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getTwistClient } from '../../lib/api.js'
import type { MutationOptions } from '../../lib/options.js'
import { formatJson } from '../../lib/output.js'
import { assertChannelIsPublic } from '../../lib/public-channels.js'
import { resolveThreadId } from '../../lib/refs.js'

Expand All @@ -17,5 +18,11 @@ export async function markThreadDone(ref: string, options: DoneOptions): Promise
const thread = await client.threads.getThread(threadId)
await assertChannelIsPublic(thread.channelId, thread.workspaceId)
await client.inbox.archiveThread(threadId)

if (options.json) {
console.log(formatJson({ id: threadId, isArchived: true }))
return
}

console.log(`Thread ${threadId} archived.`)
}
6 changes: 6 additions & 0 deletions src/commands/thread/reply.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getTwistClient } from '../../lib/api.js'
import { openEditor, readStdin } from '../../lib/input.js'
import type { MutationOptions } from '../../lib/options.js'
import { formatJson } from '../../lib/output.js'
import { assertChannelIsPublic } from '../../lib/public-channels.js'
import { extractId, resolveThreadId } from '../../lib/refs.js'

Expand Down Expand Up @@ -66,5 +67,10 @@ export async function replyToThread(
recipients,
} as Parameters<typeof client.comments.createComment>[0])

if (options.json) {
console.log(formatJson(comment, 'comment', options.full))
return
}

console.log(`Comment posted: ${comment.url}`)
}
2 changes: 2 additions & 0 deletions src/lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export type PaginatedViewOptions = ViewOptions & {

export type MutationOptions = {
dryRun?: boolean
json?: boolean
full?: boolean
}
6 changes: 6 additions & 0 deletions src/lib/skills/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ tw thread view <ref> --raw # Show raw markdown
tw thread reply <ref> "content" # Post a comment
tw thread reply <ref> "content" --notify EVERYONE # Notify all workspace members
tw thread reply <ref> "content" --notify 123,id:456 # Notify specific user IDs
tw thread reply <ref> "content" --json # Post and return comment as JSON
tw thread done <ref> # Archive thread (mark done)
tw thread done <ref> --json # Archive and return status as JSON
\`\`\`

Default \`--notify\` is EVERYONE_IN_THREAD. Options: EVERYONE, EVERYONE_IN_THREAD, or comma-separated user ID refs.
Expand All @@ -75,7 +77,9 @@ tw conversation with <user-ref> # Find your 1:1 DM with a user
tw conversation with <user-ref> --snippet # Include the latest message preview
tw conversation with <user-ref> --include-groups # List any conversations with that user
tw conversation reply <ref> "content" # Send a message
tw conversation reply <ref> "content" --json # Send and return message as JSON
tw conversation done <ref> # Archive conversation
tw conversation done <ref> --json # Archive and return status as JSON
\`\`\`

Alias: \`tw convo\` works the same as \`tw conversation\`.
Expand All @@ -86,7 +90,9 @@ Alias: \`tw convo\` works the same as \`tw conversation\`.
tw msg <message-ref> # View a message (shorthand for view)
tw msg view <message-ref> # View a single conversation message
tw msg update <ref> "content" # Edit a conversation message
tw msg update <ref> "content" --json # Edit and return updated message as JSON
tw msg delete <ref> # Delete a conversation message
tw msg delete <ref> --json # Delete and return status as JSON
\`\`\`

Alias: \`tw message\` works the same as \`tw msg\`.
Expand Down
Loading