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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ beep

# Logs
*.log

# Local agent state
.serena/
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,18 @@ beepctl messages <chat-id> # List recent messages
beepctl messages <chat-id> --limit 20 # Limit results
beepctl messages <chat-id> --after "1d ago" # Messages after a time
beepctl messages <chat-id> --before "1h ago" # Messages before a time
beepctl messages <chat-id> --json # JSON output (includes attachment mxc URLs)
beepctl messages work # Use alias
```

Download attachments from recent messages with `jq`:

```bash
beepctl messages <chat-id> --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:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
71 changes: 63 additions & 8 deletions src/commands/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const messagesCommand = new Command("messages")
.option("-l, --limit <number>", "Maximum messages to show", "20")
.option("--after <date>", "Messages after date (e.g., '1d ago', 'yesterday')")
.option("--before <date>", "Messages before date (e.g., '1h ago', 'today')")
.option("--json", "Output messages as JSON")
.action(async (chatIdArg: string, options) => {
try {
const client = getClient();
Expand All @@ -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<string, string>();
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
Expand All @@ -58,13 +54,30 @@ 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"));
}
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<string, string>();
for (const account of accounts) {
networkMap.set(account.accountID, account.network || account.accountID);
}

console.log(kleur.bold(`\nMessages (${messages.length})`));
console.log(SEPARATOR);

Expand Down Expand Up @@ -96,7 +109,9 @@ function printMessage(msg: Message, index: number, networkMap: Map<string, strin
const icon = getAttachmentIcon(att.type);
const size = att.fileSize ? ` (${formatSize(att.fileSize)})` : "";
const name = att.fileName || att.type || "attachment";
console.log(kleur.dim(` ${icon} ${name}${size}`));
const mxcUrl = getAttachmentMxcUrl(att);
const url = mxcUrl ? ` ${mxcUrl}` : "";
console.log(kleur.dim(` ${icon} ${name}${size}${url}`));
}
}

Expand All @@ -119,3 +134,43 @@ function getAttachmentIcon(type?: string): string {
return "att";
}
}

function getAttachmentMxcUrl(attachment: unknown): string | undefined {
if (!attachment || typeof attachment !== "object") return undefined;

const entry = attachment as Record<string, unknown>;
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<Message["attachments"]>[number]) {
return {
...att,
mxcUrl: getAttachmentMxcUrl(att),
};
}