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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ 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.6] - 2026-03-23

### Added

- Added `beepctl react <chat-id> <message-id> <reaction-key>` to add emoji, shortcode, or custom emoji reactions to existing messages.
- Added `beepctl react --remove` to remove the authenticated user's reaction for a reaction key.
- Added focused command tests covering add, remove, alias resolution, quiet mode, and shared write-permission errors.
- Updated README and packaged skill docs with reaction command usage and semantics.

### Changed

- Upgraded `@beeper/desktop-api` to `4.7.0` to use the official SDK reaction methods exposed at `client.chats.messages.reactions.*`.


## [0.1.5] - 2026-02-20

### Added
Expand Down Expand Up @@ -58,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Migrated to official `@beeper/desktop-api` SDK

[0.1.6]: https://github.com/blqke/beepctl/releases/tag/v0.1.6
[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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,20 @@ beepctl reminders set <chat-id> tomorrow # Remind tomorrow
beepctl reminders clear <chat-id> # Clear reminder
```

### React to Messages

Add or remove emoji reactions on messages:

```bash
beepctl react <chat-id> <message-id> 👍 # React with emoji
beepctl react <chat-id> <message-id> thumbsup # React with shortcode
beepctl react <chat-id> <message-id> custom_emoji_key # React with custom emoji key
beepctl react <chat-id> <message-id> 👍 --remove # Remove your reaction
beepctl react work <message-id> ❤️ # Use alias
```

`<reaction-key>` can be an emoji character, a shortcode (e.g. `thumbsup`), or a custom emoji key. `--remove` removes the authenticated user's reaction for that key — it cannot remove other users' reactions.

## Development

```bash
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "beepctl",
"version": "0.1.5",
"version": "0.1.6",
"description": "CLI for Beeper Desktop API - unified messaging from terminal",
"license": "MIT",
"type": "module",
Expand All @@ -25,7 +25,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@beeper/desktop-api": "^4.2.3",
"@beeper/desktop-api": "^4.7.0",
"commander": "^14.0.0",
"kleur": "^4.1.5"
},
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions skills/beepctl/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ beepctl reminders set <chat> tomorrow # Remind tomorrow
beepctl reminders clear <chat> # Clear reminder
```

### React to Messages
```bash
beepctl react <chat-id> <message-id> 👍 # React with emoji
beepctl react <chat-id> <message-id> thumbsup # React with shortcode
beepctl react <chat-id> <message-id> custom_emoji_key # React with custom emoji key
beepctl react <chat-id> <message-id> 👍 --remove # Remove your reaction
beepctl react work <message-id> ❤️ # Use alias
```

`<reaction-key>` accepts an emoji, shortcode, or custom emoji key. `--remove` removes the authenticated user's reaction only — it cannot remove other users' reactions.

## Tips

- Chat IDs look like: `!gZ42vWzDxl8V0sZXWBgO:beeper.local`
Expand Down
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { contactsCommand } from "./commands/contacts.js";
import { downloadCommand } from "./commands/download.js";
import { focusCommand } from "./commands/focus.js";
import { messagesCommand } from "./commands/messages.js";
import { reactCommand } from "./commands/react.js";
import { remindersCommand } from "./commands/reminders.js";
import { searchCommand } from "./commands/search.js";
import { sendCommand } from "./commands/send.js";
Expand All @@ -29,6 +30,7 @@ program
.addCommand(downloadCommand)
.addCommand(focusCommand)
.addCommand(messagesCommand)
.addCommand(reactCommand)
.addCommand(remindersCommand)
.addCommand(sendCommand)
.addCommand(searchCommand)
Expand Down
4 changes: 2 additions & 2 deletions src/commands/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from "commander";
import kleur from "kleur";
import { getClient } from "../lib/client.js";
import { getAccountNetwork, getClient } from "../lib/client.js";
import { handleError } from "../lib/errors.js";

export const accountsCommand = new Command("accounts")
Expand All @@ -20,7 +20,7 @@ export const accountsCommand = new Command("accounts")

for (const account of accounts) {
const name = account.user?.fullName || account.user?.username || account.accountID;
console.log(` ${kleur.cyan(account.network)} ${kleur.bold(name)}`);
console.log(` ${kleur.cyan(getAccountNetwork(account))} ${kleur.bold(name)}`);
console.log(kleur.dim(` ID: ${account.accountID}\n`));
}
} catch (error) {
Expand Down
13 changes: 7 additions & 6 deletions src/commands/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from "commander";
import kleur from "kleur";
import { isValidChatId, resolveAlias } from "../lib/aliases.js";
import type { ChatListResponse } from "../lib/client.js";
import { getClient } from "../lib/client.js";
import { getChatDescription, getChatNetwork, getClient } from "../lib/client.js";
import { getConfig } from "../lib/config.js";
import { parseRelativeDate } from "../lib/dates.js";
import { handleError } from "../lib/errors.js";
Expand Down Expand Up @@ -65,10 +65,11 @@ chatsCommand
console.log(`${kleur.dim("ID:")} ${chat.id}`);
console.log(`${kleur.dim("Title:")} ${chat.title || "(none)"}`);
console.log(`${kleur.dim("Type:")} ${chat.type}`);
console.log(`${kleur.dim("Network:")} ${chat.network}`);
console.log(`${kleur.dim("Network:")} ${getChatNetwork(chat)}`);
console.log(`${kleur.dim("Account:")} ${chat.accountID}`);
if (chat.description) {
console.log(`${kleur.dim("Description:")} ${chat.description}`);
const description = getChatDescription(chat);
if (description) {
console.log(`${kleur.dim("Description:")} ${description}`);
}
console.log(SEPARATOR);
console.log(`${kleur.dim("Unread:")} ${chat.unreadCount}`);
Expand Down Expand Up @@ -254,8 +255,8 @@ function printChatList(
for (let i = 0; i < chats.length; i++) {
const chat = chats[i];
const num = kleur.dim(`${i + 1}.`);
const name = kleur.bold(chat.title || chat.description || "Unknown");
const network = kleur.dim(`[${chat.network || chat.accountID}]`);
const name = kleur.bold(chat.title || getChatDescription(chat) || "Unknown");
const network = kleur.dim(`[${getChatNetwork(chat)}]`);
const unread = chat.unreadCount ? kleur.red(` (${chat.unreadCount} unread)`) : "";

console.log(`${num} ${name} ${network}${unread}`);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from "commander";
import kleur from "kleur";
import type { Message } from "../lib/client.js";
import { getClient } from "../lib/client.js";
import { getAccountNetwork, getClient } from "../lib/client.js";
import {
formatSize,
handleCommandError,
Expand Down Expand Up @@ -75,7 +75,7 @@ export const messagesCommand = new Command("messages")
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);
networkMap.set(account.accountID, getAccountNetwork(account));
}

console.log(kleur.bold(`\nMessages (${messages.length})`));
Expand Down
56 changes: 56 additions & 0 deletions src/commands/react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Command } from "commander";
import kleur from "kleur";
import { getClient } from "../lib/client.js";
import {
handleCommandError,
resolveChatIdOrExit,
WRITE_PERMISSION_ERROR_HANDLER,
} from "../lib/command-utils.js";
import { getConfig } from "../lib/config.js";

export const reactCommand = new Command("react")
.description("Add or remove your reaction on an existing message")
.argument("<chat-id>", "Chat ID or alias")
.argument("<message-id>", "Message ID to react to")
.argument("<reaction-key>", "Reaction key (emoji, shortcode, or custom emoji key)")
.option("--remove", "Remove your reaction instead of adding it")
.option("-q, --quiet", "Don't show confirmation")
.action(async (chatIdArg: string, messageId: string, reactionKey: string, options) => {
try {
const client = getClient();
const config = getConfig();
const chatID = resolveChatIdOrExit(chatIdArg, config);

if (options.remove) {
const removed = await client.chats.messages.reactions.delete(messageId, {
chatID,
reactionKey,
});

if (!options.quiet) {
console.log(kleur.green("Reaction removed"));
console.log(kleur.dim(` Chat: ${removed.chatID}`));
console.log(kleur.dim(` Message: ${removed.messageID}`));
console.log(kleur.dim(` Reaction: ${removed.reactionKey}`));
}
return;
}

const added = await client.chats.messages.reactions.add(messageId, {
chatID,
reactionKey,
});

if (!options.quiet) {
console.log(kleur.green("Reaction added"));
console.log(kleur.dim(` Chat: ${added.chatID}`));
console.log(kleur.dim(` Message: ${added.messageID}`));
console.log(kleur.dim(` Reaction: ${added.reactionKey}`));
if (added.transactionID) {
console.log(kleur.dim(` Transaction: ${added.transactionID}`));
}
}
} catch (error) {
handleCommandError(error, [WRITE_PERMISSION_ERROR_HANDLER]);
}
});
6 changes: 3 additions & 3 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from "commander";
import kleur from "kleur";
import { isValidChatId, resolveAlias } from "../lib/aliases.js";
import type { Chat, Message } from "../lib/client.js";
import { getClient } from "../lib/client.js";
import { getAccountNetwork, getChatDescription, getClient } from "../lib/client.js";
import {
handleCommandError,
highlightQuery,
Expand Down Expand Up @@ -68,7 +68,7 @@ export const searchCommand = new Command("search")
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);
networkMap.set(account.accountID, getAccountNetwork(account));
}

// Build search params
Expand Down Expand Up @@ -237,7 +237,7 @@ function printChats(chats: Chat[]): void {
for (let i = 0; i < chats.length; i++) {
const chat = chats[i];
const num = kleur.dim(`${i + 1}.`);
console.log(`${num} ${kleur.bold(chat.title || chat.description || "Unknown")}`);
console.log(`${num} ${kleur.bold(chat.title || getChatDescription(chat) || "Unknown")}`);
console.log(kleur.dim(` ID: ${chat.id}`));
if (i < chats.length - 1) console.log(THIN_SEP);
}
Expand Down
10 changes: 2 additions & 8 deletions src/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from "commander";
import kleur from "kleur";
import { resolveAlias } from "../lib/aliases.js";
import { getClient } from "../lib/client.js";
import { handleCommandError } from "../lib/command-utils.js";
import { handleCommandError, WRITE_PERMISSION_ERROR_HANDLER } from "../lib/command-utils.js";
import { getConfig } from "../lib/config.js";

export const sendCommand = new Command("send")
Expand Down Expand Up @@ -32,13 +32,7 @@ export const sendCommand = new Command("send")
}
}
} catch (error) {
handleCommandError(error, [
{
match: "403",
message: "Permission denied",
hint: "Enable write permissions: Settings -> Developers -> Edit token -> Enable 'write' scope",
},
]);
handleCommandError(error, [WRITE_PERMISSION_ERROR_HANDLER]);
}
});

Expand Down
18 changes: 18 additions & 0 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ export type {
export type { Chat, ChatListResponse } from "@beeper/desktop-api/resources/chats/chats.js";
export type { Message } from "@beeper/desktop-api/resources/shared.js";

function readOptionalStringField(value: unknown, field: string): string | undefined {
if (!value || typeof value !== "object") return undefined;
const record = value as Record<string, unknown>;
return typeof record[field] === "string" ? record[field] : undefined;
}

export function getAccountNetwork(account: { accountID: string }): string {
return readOptionalStringField(account, "network") ?? account.accountID;
}

export function getChatDescription(chat: unknown): string | undefined {
return readOptionalStringField(chat, "description");
}

export function getChatNetwork(chat: { accountID: string }): string {
return readOptionalStringField(chat, "network") ?? chat.accountID;
}

let _client: BeeperDesktop | null = null;

export function getClient(): BeeperDesktop {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/command-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export function validateDateRangeOrExit(dateAfter: string, dateBefore: string):
}
}

export const WRITE_PERMISSION_ERROR_HANDLER: ErrorHandler = {
match: "403",
message: "Permission denied",
hint: "Enable write permissions: Settings -> Developers -> Edit token -> Enable 'write' scope",
};

/**
* Standard error handler for command actions.
*/
Expand Down Expand Up @@ -69,7 +75,7 @@ export function handleCommandError(error: unknown, extraHandlers?: ErrorHandler[
process.exit(1);
}

interface ErrorHandler {
export interface ErrorHandler {
match: string;
message: string;
hint?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = "0.1.0";
export const version = "0.1.6";
Loading
Loading