/** * `dench notifications ` — the signed-in member's inbox (what * the bell in the workspace rail shows), from the command line. * * Reads `functions/notifications:*` with the same bearer every other CLI * surface forwards (`dch_agent_*` session or a Dench API key). Agent * sessions are user-bound and read their owner's inbox; an org API key * with no person behind it is refused by the backend. * * dench notifications list [--filter inbox|unread|archived|all|mentions] [--limit N] [--json] * dench notifications count * dench notifications read [--unread] * dench notifications read-all * dench notifications archive * dench notifications prefs [--set key=value]... */ import type { ConvexHttpClient } from "convex/browser"; import { makeFunctionReference } from "convex/server"; import { assertNoUnknownFlags, CliArgError, getFlag, hasFlag, shift, } from "./lib/cli-args"; class NotificationsCliError extends Error {} type Ctx = { convex: ConvexHttpClient; args: string[]; jsonOutput: boolean; sessionToken?: string; }; const api = { list: makeFunctionReference<"query">( "functions/notifications:listMyNotifications", ), unreadCount: makeFunctionReference<"query">( "functions/notifications:unreadCount", ), setRead: makeFunctionReference<"mutation">( "functions/notifications:setNotificationRead", ), markAllRead: makeFunctionReference<"mutation">( "functions/notifications:markAllRead", ), archive: makeFunctionReference<"mutation">( "functions/notifications:archiveNotification", ), getPrefs: makeFunctionReference<"query">( "functions/notifications:getMyNotificationPreferences", ), updatePrefs: makeFunctionReference<"mutation">( "functions/notifications:updateMyNotificationPreferences", ), }; const FILTERS = new Set(["inbox", "unread", "archived", "all", "mentions"]); const HELP = `dench notifications — your notification inbox dench notifications list [--filter inbox|unread|archived|all|mentions] [--limit N] [--json] dench notifications count dench notifications read [--unread] dench notifications read-all dench notifications archive dench notifications prefs dench notifications prefs --set emailEnabled=false --set entry_activity.email=false Kinds: mention (includes assignment), comment_reply, comment_reaction, entry_activity, dench_reply, task_completed, date_reminder, automation, system. task_assigned is a legacy assignment kind; new assignments are mention. Comment links carry ?entry=:&comment=.`; function withToken(ctx: Ctx, args: Record) { return { ...args, ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}), }; } function out(_ctx: Ctx, value: unknown) { console.log(JSON.stringify(value, null, 2)); } function toCliError(error: unknown): Error { if (error instanceof CliArgError) return new NotificationsCliError(error.message); const data = (error as { data?: unknown } | null)?.data; if (typeof data === "string" && data.trim()) { return new NotificationsCliError(data.trim()); } return error instanceof Error ? error : new Error(String(error)); } /** `--set a=b --set c.d=false` → nested preference patch. */ function parsePrefSets(args: string[]): Record { const patch: Record = {}; const perKind: Record = {}; for (;;) { const raw = getFlag(args, "--set"); if (raw === undefined) break; const eq = raw.indexOf("="); if (eq <= 0) throw new NotificationsCliError(`--set expects key=value, got "${raw}"`); const key = raw.slice(0, eq).trim(); const value = raw.slice(eq + 1).trim(); const bool = value === "true" ? true : value === "false" ? false : undefined; const dot = key.indexOf("."); if (dot > 0) { const kind = key.slice(0, dot); const channel = key.slice(dot + 1); if ((channel !== "inApp" && channel !== "email") || bool === undefined) { throw new NotificationsCliError( `--set ${key}: use .inApp=true|false or .email=true|false`, ); } perKind[kind] = { ...(perKind[kind] ?? {}), [channel]: bool }; continue; } switch (key) { case "inAppEnabled": case "emailEnabled": if (bool === undefined) { throw new NotificationsCliError(`--set ${key} expects true|false`); } patch[key] = bool; break; case "digestWindowMinutes": case "maxEmailsPerDay": { const n = Number.parseInt(value, 10); if (!Number.isFinite(n)) { throw new NotificationsCliError(`--set ${key} expects an integer`); } patch[key] = n; break; } case "timezone": patch.timezone = value; break; default: throw new NotificationsCliError( `Unknown preference "${key}". Use inAppEnabled, emailEnabled, digestWindowMinutes, maxEmailsPerDay, timezone, or .inApp / .email.`, ); } } if (Object.keys(perKind).length > 0) patch.perKind = perKind; return patch; } export async function runNotificationsCommand(opts: { convex: ConvexHttpClient; args: string[]; sessionToken?: string; }): Promise { const args = [...opts.args]; const ctx: Ctx = { convex: opts.convex, args, jsonOutput: hasFlag(args, "--json"), sessionToken: opts.sessionToken, }; const verb = args.shift(); if (!verb || verb === "help" || verb === "--help") { console.log(HELP); return; } try { switch (verb) { case "list": { const filter = getFlag(args, "--filter") ?? "inbox"; if (!FILTERS.has(filter)) { throw new NotificationsCliError( "--filter must be inbox, unread, archived, all, or mentions", ); } const rawLimit = getFlag(args, "--limit"); const limit = rawLimit ? Number.parseInt(rawLimit, 10) : undefined; assertNoUnknownFlags(args, "notifications list"); out( ctx, await ctx.convex.query( api.list, withToken(ctx, { filter, ...(limit && Number.isFinite(limit) ? { limit } : {}), }) as never, ), ); return; } case "count": { assertNoUnknownFlags(args, "notifications count"); const count = await ctx.convex.query( api.unreadCount, withToken(ctx, {}) as never, ); out(ctx, { unread: count }); return; } case "read": { const notificationId = shift(args, "notification id"); const unread = hasFlag(args, "--unread"); assertNoUnknownFlags(args, "notifications read"); out( ctx, await ctx.convex.mutation( api.setRead, withToken(ctx, { notificationId, read: !unread }) as never, ), ); return; } case "read-all": { assertNoUnknownFlags(args, "notifications read-all"); out( ctx, await ctx.convex.mutation( api.markAllRead, withToken(ctx, {}) as never, ), ); return; } case "archive": { const notificationId = shift(args, "notification id"); assertNoUnknownFlags(args, "notifications archive"); out( ctx, await ctx.convex.mutation( api.archive, withToken(ctx, { notificationId }) as never, ), ); return; } case "prefs": { const patch = parsePrefSets(args); assertNoUnknownFlags(args, "notifications prefs"); if (Object.keys(patch).length > 0) { await ctx.convex.mutation( api.updatePrefs, withToken(ctx, patch) as never, ); } out( ctx, await ctx.convex.query(api.getPrefs, withToken(ctx, {}) as never), ); return; } default: throw new NotificationsCliError( `Unknown notifications verb: ${verb}. Use list | count | read | read-all | archive | prefs.`, ); } } catch (error) { throw toCliError(error); } }