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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ vi.mock("../../src/client.js", () => ({
post: vi.fn(),
}));

import { notificationsCommand } from "../../src/commands/notifications.js";
import { emailsCommand } from "../../src/commands/emails.js";
import { post } from "../../src/client.js";

let logSpy: ReturnType<typeof vi.spyOn>;
Expand All @@ -24,70 +24,70 @@ afterEach(() => {
vi.restoreAllMocks();
});

describe("notifications command", () => {
it("sends notification with subject and text", async () => {
describe("emails command", () => {
it("sends an email with subject and text", async () => {
vi.mocked(post).mockResolvedValue({
success: true,
message: "Email sent successfully.",
id: "email-123",
});

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
["--subject", "Test Subject", "--text", "Hello world"],
{ from: "user" },
);

expect(post).toHaveBeenCalledWith("/api/notifications", {
expect(post).toHaveBeenCalledWith("/api/emails", {
subject: "Test Subject",
text: "Hello world",
});
expect(logSpy).toHaveBeenCalledWith("Email sent successfully.");
});

it("sends notification with html body", async () => {
it("sends an email with html body", async () => {
vi.mocked(post).mockResolvedValue({
success: true,
message: "Email sent successfully.",
id: "email-456",
});

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
["--subject", "Weekly Pulse", "--html", "<h1>Report</h1>"],
{ from: "user" },
);

expect(post).toHaveBeenCalledWith("/api/notifications", {
expect(post).toHaveBeenCalledWith("/api/emails", {
subject: "Weekly Pulse",
html: "<h1>Report</h1>",
});
});

it("passes cc and room-id options", async () => {
it("passes cc and chat-id options", async () => {
vi.mocked(post).mockResolvedValue({
success: true,
message: "Email sent successfully.",
id: "email-789",
});

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
[
"--subject",
"Update",
"--text",
"Hello",
"--cc",
"cc@example.com",
"--room-id",
"room-abc",
"--chat-id",
"chat-abc",
],
{ from: "user" },
);

expect(post).toHaveBeenCalledWith("/api/notifications", {
expect(post).toHaveBeenCalledWith("/api/emails", {
subject: "Update",
text: "Hello",
cc: ["cc@example.com"],
room_id: "room-abc",
chat_id: "chat-abc",
});
});

Expand All @@ -98,7 +98,7 @@ describe("notifications command", () => {
id: "email-multi",
});

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
[
"--subject",
"Update",
Expand All @@ -110,7 +110,7 @@ describe("notifications command", () => {
{ from: "user" },
);

expect(post).toHaveBeenCalledWith("/api/notifications", {
expect(post).toHaveBeenCalledWith("/api/emails", {
subject: "Update",
cc: ["a@example.com", "b@example.com"],
});
Expand All @@ -124,7 +124,7 @@ describe("notifications command", () => {
};
vi.mocked(post).mockResolvedValue(response);

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
["--subject", "Test", "--json"],
{ from: "user" },
);
Expand All @@ -141,7 +141,7 @@ describe("notifications command", () => {
id: "email-account",
});

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
[
"--subject",
"Override Test",
Expand All @@ -153,7 +153,7 @@ describe("notifications command", () => {
{ from: "user" },
);

expect(post).toHaveBeenCalledWith("/api/notifications", {
expect(post).toHaveBeenCalledWith("/api/emails", {
subject: "Override Test",
text: "Hello member",
account_id: "550e8400-e29b-41d4-a716-446655440000",
Expand All @@ -167,20 +167,36 @@ describe("notifications command", () => {
id: "email-no-account",
});

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
["--subject", "Test"],
{ from: "user" },
);

expect(post).toHaveBeenCalledWith("/api/notifications", {
expect(post).toHaveBeenCalledWith("/api/emails", {
subject: "Test",
});
});

it("omits subject when --subject is not provided (now optional)", async () => {
vi.mocked(post).mockResolvedValue({
success: true,
message: "Email sent successfully.",
id: "email-no-subject",
});

await emailsCommand.parseAsync(["--text", "# Pulse Report\n\nbody"], {
from: "user",
});

expect(post).toHaveBeenCalledWith("/api/emails", {
text: "# Pulse Report\n\nbody",
});
});

it("prints error on failure", async () => {
vi.mocked(post).mockRejectedValue(new Error("No email address found"));

await notificationsCommand.parseAsync(
await emailsCommand.parseAsync(
["--subject", "Test"],
{ from: "user" },
);
Expand Down
4 changes: 2 additions & 2 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { artistsCommand } from "./commands/artists.js";
import { chatsCommand } from "./commands/chats.js";
import { sandboxesCommand } from "./commands/sandboxes.js";
import { songsCommand } from "./commands/songs.js";
import { notificationsCommand } from "./commands/notifications.js";
import { emailsCommand } from "./commands/emails.js";
import { orgsCommand } from "./commands/orgs.js";
import { contentCommand } from "./commands/content.js";
import { tasksCommand } from "./commands/tasks.js";
Expand All @@ -25,7 +25,7 @@ program.addCommand(whoamiCommand);
program.addCommand(artistsCommand);
program.addCommand(chatsCommand);
program.addCommand(songsCommand);
program.addCommand(notificationsCommand);
program.addCommand(emailsCommand);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag AI Slop and Fabricated Changes

PR description claims the notifications command name is unchanged, but the code actually renames it to emails, contradicting the documented behavior and breaking existing users of recoup notifications.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/bin.ts, line 28:

<comment>PR description claims the `notifications` command name is unchanged, but the code actually renames it to `emails`, contradicting the documented behavior and breaking existing users of `recoup notifications`.</comment>

<file context>
@@ -25,7 +25,7 @@ program.addCommand(whoamiCommand);
 program.addCommand(chatsCommand);
 program.addCommand(songsCommand);
-program.addCommand(notificationsCommand);
+program.addCommand(emailsCommand);
 program.addCommand(sandboxesCommand);
 program.addCommand(orgsCommand);
</file context>

program.addCommand(sandboxesCommand);
program.addCommand(orgsCommand);
program.addCommand(tasksCommand);
Expand Down
19 changes: 9 additions & 10 deletions src/commands/notifications.ts → src/commands/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,31 @@ import { Command } from "commander";
import { post } from "../client.js";
import { printJson, printError } from "../output.js";

export const notificationsCommand = new Command("notifications")
.description("Send an email to the account owner. The recipient is automatically resolved from your API key — no --to flag needed. Only --subject is required.")
.requiredOption("--subject <text>", "Email subject line")
export const emailsCommand = new Command("emails")
.description("Send an email to the account owner. The recipient is automatically resolved from your API key — no --to flag needed. --subject is optional (defaults from the body).")
.option("--subject <text>", "Email subject line (optional; defaults from the body)")
.option("--text <body>", "Plain text or Markdown body")
.option("--html <body>", "Raw HTML body (takes precedence over --text)")
.option("--cc <email>", "CC recipient (repeatable)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
.option("--room-id <id>", "Room ID for chat link in footer")
.option("--chat-id <id>", "Chat ID for chat link in footer")
.option("--account <accountId>", "Send to a specific account (org keys only)")
.option("--json", "Output as JSON")
.action(async (opts) => {
try {
const body: Record<string, unknown> = {
subject: opts.subject,
};
const body: Record<string, unknown> = {};
if (opts.subject) body.subject = opts.subject;
if (opts.text) body.text = opts.text;
if (opts.html) body.html = opts.html;
if (opts.cc && opts.cc.length > 0) body.cc = opts.cc;
if (opts.roomId) body.room_id = opts.roomId;
if (opts.chatId) body.chat_id = opts.chatId;
if (opts.account) body.account_id = opts.account;

const data = await post("/api/notifications", body);
const data = await post("/api/emails", body);

if (opts.json) {
printJson(data);
} else {
console.log(data.message || "Notification sent.");
console.log(data.message || "Email sent.");
}
} catch (err) {
printError((err as Error).message);
Expand Down
Loading