/** * `dench crm privacy …` — the caller's per-record privacy opt-out. * * Shared-by-default (2026-09-25): everything a member connects syncs to the * workspace; privacy is an opt-out per connection ("Private" on the card) or * per record, here. Hiding a record hides the CALLER's own emails, LinkedIn * messages and calendar events that involve it — an account covers every * contact and opportunity linked to it — for other members only. Colleagues' * threads about the same record are untouched. Existing ledger rows are * restamped and the record's shared Brief regenerates without them. * * status [--json] * hide [--json] * show [--json] * * `` is accepted for symmetry with the other record verbs; the * opt-out is keyed on the entry id alone. */ import { makeFunctionReference } from "convex/server"; import { assertNoUnknownFlags, type CrmCliContext, CrmCliError, callMutation, callQuery, out, shift, } from "./crm"; const api = { get: makeFunctionReference<"query">( "functions/crm/recordPrivacy:getRecordPrivacy", ), set: makeFunctionReference<"mutation">( "functions/crm/recordPrivacy:setRecordPrivacy", ), }; export type RecordPrivacyStatus = { hidden: boolean; inheritedFromEntryId?: string; inheritedFromLabel?: string; hasOwnConnection: boolean; }; export type RecordPrivacySetResult = { hidden: boolean; restamped: number; continued: boolean; }; export const PRIVACY_USAGE = "Usage: dench crm privacy status|hide|show [--json]"; export function formatPrivacyStatus(status: RecordPrivacyStatus): string { if (!status.hasOwnConnection) { return "You have no connection of your own here, so there is nothing to hide — connect an inbox, LinkedIn or calendar first."; } if (status.hidden && status.inheritedFromEntryId) { return `Hidden with ${status.inheritedFromLabel ?? "its account"} (${status.inheritedFromEntryId}). Show the account to show this record.`; } return status.hidden ? "Hidden — only you see your emails and messages with this record." : "Shared — teammates see your emails and messages with this record."; } export function formatPrivacySet(result: RecordPrivacySetResult): string { const rows = result.restamped === 1 ? "1 activity row" : `${result.restamped} activity rows`; const tail = result.continued ? " More rows are being restamped in the background." : ""; return result.hidden ? `Hidden. ${rows} moved to only you; the shared Brief will regenerate.${tail}` : `Shown. ${rows} opened to the workspace; the shared Brief will regenerate.${tail}`; } export async function runPrivacyCommand(ctx: CrmCliContext): Promise { const verb = ctx.args.shift(); if (verb !== "status" && verb !== "hide" && verb !== "show") { throw new CrmCliError(PRIVACY_USAGE); } shift(ctx.args, "object name"); const entryId = shift(ctx.args, "entry id"); assertNoUnknownFlags(ctx.args, `crm privacy ${verb}`); if (verb === "status") { const status = (await callQuery(ctx, api.get, { entryId, })) as RecordPrivacyStatus; out(ctx, ctx.jsonOutput ? status : formatPrivacyStatus(status)); return; } const result = (await callMutation(ctx, api.set, { entryId, hidden: verb === "hide", })) as RecordPrivacySetResult; out(ctx, ctx.jsonOutput ? result : formatPrivacySet(result)); }