diff --git a/.gitignore b/.gitignore index f536dbc..194a811 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ beep # Logs *.log + +# Local agent state +.serena/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a725af9..3b67a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.5] - 2026-02-20 + +### Added + +- Added `beepctl messages --json` output for scripting workflows, including attachment `mxcUrl` fields. +- Added MXC URL display in `beepctl messages` text output for attachments when available. +- Added README example showing `messages --json | jq | download` attachment pipeline. + +### Fixed + +- Improved attachment MXC URL detection to support `srcURL` and `id` fallback fields. +- Fixed `messages --json` attachment typing to keep TypeScript compatibility. + ## [0.1.3] - 2025-01-25 ### Changed @@ -45,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Migrated to official `@beeper/desktop-api` SDK +[0.1.5]: https://github.com/blqke/beepctl/releases/tag/v0.1.5 [0.1.3]: https://github.com/blqke/beepctl/releases/tag/v0.1.3 [0.1.2]: https://github.com/blqke/beepctl/releases/tag/v0.1.2 [0.1.1]: https://github.com/blqke/beepctl/releases/tag/v0.1.1 diff --git a/README.md b/README.md index f2539bf..dd899ba 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,18 @@ beepctl messages # List recent messages beepctl messages --limit 20 # Limit results beepctl messages --after "1d ago" # Messages after a time beepctl messages --before "1h ago" # Messages before a time +beepctl messages --json # JSON output (includes attachment mxc URLs) beepctl messages work # Use alias ``` +Download attachments from recent messages with `jq`: + +```bash +beepctl messages --limit 50 --json \ + | jq -r '.[].attachments[]?.mxcUrl | select(startswith("mxc://") or startswith("localmxc://"))' \ + | while read -r url; do beepctl download "$url"; done +``` + ### Search Search messages across all chats: diff --git a/package.json b/package.json index f700d6a..22a25ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "beepctl", - "version": "0.1.4", + "version": "0.1.5", "description": "CLI for Beeper Desktop API - unified messaging from terminal", "license": "MIT", "type": "module", diff --git a/src/commands/messages.ts b/src/commands/messages.ts index 71620c6..da8d3cd 100644 --- a/src/commands/messages.ts +++ b/src/commands/messages.ts @@ -18,6 +18,7 @@ export const messagesCommand = new Command("messages") .option("-l, --limit ", "Maximum messages to show", "20") .option("--after ", "Messages after date (e.g., '1d ago', 'yesterday')") .option("--before ", "Messages before date (e.g., '1h ago', 'today')") + .option("--json", "Output messages as JSON") .action(async (chatIdArg: string, options) => { try { const client = getClient(); @@ -38,13 +39,8 @@ export const messagesCommand = new Command("messages") if (options.after) filterParts.push(`after: ${options.after}`); if (options.before) filterParts.push(`before: ${options.before}`); const filters = filterParts.length > 0 ? kleur.dim(` [${filterParts.join(", ")}]`) : ""; - console.log(kleur.dim(`Listing messages from chat ${chatID}${filters}...`)); - - // Build accountID -> network map - const accounts = await client.accounts.list(); - const networkMap = new Map(); - for (const account of accounts) { - networkMap.set(account.accountID, account.network || account.accountID); + if (!options.json) { + console.log(kleur.dim(`Listing messages from chat ${chatID}${filters}...`)); } // Fetch messages with client-side date filtering @@ -58,6 +54,11 @@ export const messagesCommand = new Command("messages") } if (messages.length === 0) { + if (options.json) { + console.log("[]"); + return; + } + console.log(kleur.yellow(`\nNo messages found in chat ${chatID}`)); if (options.after || options.before) { console.log(kleur.dim(" Try adjusting the date filters")); @@ -65,6 +66,18 @@ export const messagesCommand = new Command("messages") return; } + if (options.json) { + console.log(JSON.stringify(messages.map(formatMessageForJson), null, 2)); + return; + } + + // Build accountID -> network map + const accounts = await client.accounts.list(); + const networkMap = new Map(); + for (const account of accounts) { + networkMap.set(account.accountID, account.network || account.accountID); + } + console.log(kleur.bold(`\nMessages (${messages.length})`)); console.log(SEPARATOR); @@ -96,7 +109,9 @@ function printMessage(msg: Message, index: number, networkMap: Map; + const candidates = [ + entry.url, + entry.mxcUrl, + entry.mxc, + entry.contentUrl, + entry.sourceUrl, + entry.srcURL, + entry.id, + ]; + + for (const candidate of candidates) { + if ( + typeof candidate === "string" && + (candidate.startsWith("mxc://") || candidate.startsWith("localmxc://")) + ) { + return candidate; + } + } + + return undefined; +} + +function formatMessageForJson(msg: Message): Message { + return { + ...msg, + attachments: msg.attachments?.map((att) => formatAttachmentForJson(att)), + }; +} + +function formatAttachmentForJson(att: NonNullable[number]) { + return { + ...att, + mxcUrl: getAttachmentMxcUrl(att), + }; +}