/** * `dench settings ` — workspace settings from the command line. * * Today that is Settings → Sync: the eight switches that decide what Dench * does on its own with the workspace's email, calendar, Slack and LinkedIn * (docs/agent-notes/account-context-ingestion.md §12). All on by default; * turning one off stops NEW work and never removes anything already created. * * dench settings sync [--json] the eight switches * dench settings sync set on|off [--json] admin-only * * Reads `functions/organizations:getMySyncSettings` and writes * `functions/organizations:updateMyOrganizationSyncSettings` with a one-key * patch, forwarding the same bearer every other CLI surface uses. The * backend enforces admin on the write (`requireCrmAdminAccess`), so an agent * session acting for a member is refused server-side, not here. * * The key list mirrors `SYNC_SETTING_KEYS` in `convex/lib/orgSettings.ts`; * the CLI package does not import from `convex/`, so a test pins the two. */ import type { ConvexHttpClient } from "convex/browser"; import { makeFunctionReference } from "convex/server"; import { assertNoUnknownFlags, CliArgError, hasFlag } from "./lib/cli-args"; class SettingsCliError extends Error {} type Ctx = { convex: ConvexHttpClient; args: string[]; jsonOutput: boolean; sessionToken?: string; }; /** Key → the label Settings → Sync shows, in page order. */ export const SYNC_SETTING_LABELS = { detectAccounts: "Accounts from email", linkedinContacts: "Contacts from LinkedIn", slackConnectAccounts: "Accounts from Slack Connect", calendarContacts: "Contacts from calendar", dossier: "Research dossier", emailTasks: "Tasks from email", leadAutoQualify: "Qualify leads that book", intelligence: "Summaries and Brief", } as const; export type SyncSettingKey = keyof typeof SYNC_SETTING_LABELS; export const SYNC_SETTING_KEYS = Object.keys( SYNC_SETTING_LABELS, ) as SyncSettingKey[]; const api = { getSync: makeFunctionReference<"query">( "functions/organizations:getMySyncSettings", ), updateSync: makeFunctionReference<"mutation">( "functions/organizations:updateMyOrganizationSyncSettings", ), }; const HELP = `dench settings — workspace settings dench settings sync [--json] The eight Sync switches: what Dench does on its own with your email, calendar, Slack and LinkedIn. All on by default. dench settings sync set on|off [--json] Flip one switch (workspace admins only). Off stops new work; nothing already created is removed. Keys: ${SYNC_SETTING_KEYS.map((key) => ` ${key.padEnd(22)} ${SYNC_SETTING_LABELS[key]}`).join("\n")} Keys are accepted in camelCase or kebab-case (detect-accounts).`; /** `detect-accounts` / `DetectAccounts` → `detectAccounts`; unknown → error. */ export function parseSyncSettingKey(raw: string | undefined): SyncSettingKey { const normalized = (raw ?? "") .trim() .toLowerCase() .replace(/[-_\s]/g, ""); const key = SYNC_SETTING_KEYS.find( (candidate) => candidate.toLowerCase() === normalized, ); if (!key) { throw new SettingsCliError( `Unknown Sync setting "${raw ?? ""}". One of: ${SYNC_SETTING_KEYS.join(", ")}.`, ); } return key; } /** `on|off|true|false|1|0` → boolean; anything else → error. */ export function parseSyncSettingValue(raw: string | undefined): boolean { const normalized = (raw ?? "").trim().toLowerCase(); if (["on", "true", "1", "yes", "enabled"].includes(normalized)) return true; if (["off", "false", "0", "no", "disabled"].includes(normalized)) return false; throw new SettingsCliError( `Expected on|off after the key, got "${raw ?? ""}".`, ); } function withToken(ctx: Ctx, args: Record) { return { ...args, ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}), }; } function toCliError(error: unknown): Error { if (error instanceof CliArgError) return new SettingsCliError(error.message); const data = (error as { data?: unknown } | null)?.data; if (typeof data === "string" && data.trim()) { return new SettingsCliError(data.trim()); } return error instanceof Error ? error : new Error(String(error)); } export function formatSyncSettingsTable( settings: Record, ): string { const rows = SYNC_SETTING_KEYS.map((key) => [ key, settings[key] === false ? "off" : "on", SYNC_SETTING_LABELS[key], ]); const headers = ["key", "state", "label"]; const widths = headers.map((header, idx) => Math.max(header.length, ...rows.map((row) => row[idx].length)), ); const line = (row: string[]) => row .map((cell, idx) => cell.padEnd(widths[idx])) .join(" ") .trimEnd(); return [ line(headers), widths.map((w) => "-".repeat(w)).join(" "), ...rows.map(line), ].join("\n"); } export async function runSettingsCommand(opts: { convex: ConvexHttpClient; args: string[]; sessionToken?: string; jsonOutput?: boolean; }): Promise { const args = [...opts.args]; // Drain the flag even when the dispatcher pre-resolved it, or // `assertNoUnknownFlags` rejects `--json` (AGENTS.md, CLI `--json` contract). const jsonOutput = hasFlag(args, "--json") || opts.jsonOutput === true; const ctx: Ctx = { convex: opts.convex, args, jsonOutput, sessionToken: opts.sessionToken, }; const subcommand = args.shift(); if (!subcommand || subcommand === "help" || subcommand === "--help") { console.log(HELP); return; } if (subcommand !== "sync") { throw new SettingsCliError( `Unknown settings subcommand: ${subcommand}. Run \`dench settings help\`.`, ); } const verb = args[0]; if (verb === "set") { args.shift(); return await runSyncSet(ctx); } if (verb === "get") args.shift(); return await runSyncGet(ctx); } async function runSyncGet(ctx: Ctx): Promise { assertNoUnknownFlags(ctx.args, "settings sync"); if (ctx.args.length > 0) { throw new SettingsCliError( `Unexpected argument "${ctx.args[0]}". Did you mean \`dench settings sync set on|off\`?`, ); } let settings: Record; try { settings = (await ctx.convex.query( api.getSync, withToken(ctx, {}) as never, )) as Record; } catch (error) { throw toCliError(error); } if (ctx.jsonOutput) { console.log(JSON.stringify(settings, null, 2)); return; } console.log(formatSyncSettingsTable(settings)); } async function runSyncSet(ctx: Ctx): Promise { const key = parseSyncSettingKey(ctx.args.shift()); const value = parseSyncSettingValue(ctx.args.shift()); assertNoUnknownFlags(ctx.args, "settings sync set"); if (ctx.args.length > 0) { throw new SettingsCliError(`Unexpected argument "${ctx.args[0]}".`); } let settings: Record; try { settings = (await ctx.convex.mutation( api.updateSync, withToken(ctx, { patch: { [key]: value } }) as never, )) as Record; } catch (error) { throw toCliError(error); } if (ctx.jsonOutput) { console.log(JSON.stringify(settings, null, 2)); return; } console.log( `${SYNC_SETTING_LABELS[key]} (${key}) is now ${value ? "on" : "off"}.${ value ? "" : " New work stops from here; nothing already created is removed." }`, ); console.log(""); console.log(formatSyncSettingsTable(settings)); }