import { type ZodTypeAny, z } from "zod"; import { convexExtras, requestSchemas, responseSchemas } from "./api-schemas"; export type ApiAuthMode = "none" | "bearer" | "optional"; export type ConvexOperation = { kind: "convex"; functionType: "query" | "mutation" | "action"; functionName: string; /** Explicit envelope variant; the legacy function keeps its array contract. */ withReferencesFunctionName?: string; auth: ApiAuthMode; tokenArg?: "sessionToken" | "apiKey"; /** * With `tokenArg: "apiKey"`: the Convex function validates a `dch_agent_*` * session passed as `apiKey` itself (org-scoped, like CRM reads), so an * agent session goes through as-is instead of being exchanged for the * workspace gateway key. The narrow key a `connectedAppAccess: "none"` * session (MCP, Dench Code) exchanges for carries no `files:*` scope. * Read-only operations only. */ agentSessionAsApiKey?: true; includeRunId?: boolean; /** Constant args merged into every call (e.g. enable/disable toggles). */ fixedArgs?: Record; /** Reshape validated input into backend args (e.g. update-many -> batch ops). */ transformArgs?: (input: Record) => Record; }; export type GatewayOperation = { kind: "gateway"; method: "GET" | "POST" | "DELETE"; path: string; auth: "bearer"; }; export type CustomOperation = { kind: "custom"; handler: | "commandsList" | "chatSpawn" | "chatFollow" | "campaignBuilder" | "cronDelete" | "cronRunNow" | "connectionsList" | "toolDisconnect" | "nativeToolSearch" | "nativeToolRun" | "nativeToolConnect" | "billingTopup" | "upgrade" | "filesMove" | "filesStage" | "browserAction" | "signinOtpStart" | "signinOtpVerify" | "signinOtpFinalize"; auth: ApiAuthMode; }; export type LocalOnlyOperation = { kind: "local"; reason: string; }; export type ApiOperationBackend = | ConvexOperation | GatewayOperation | CustomOperation | LocalOnlyOperation; export type ApiOperation = { id: string; group: string; summary: string; cli: string; aliases?: string[]; method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; path: string; requestSchema: ZodTypeAny; responseSchema: ZodTypeAny; backend: ApiOperationBackend; deprecated?: boolean; }; /** Accepts any JSON body (for POST/PATCH endpoints not yet strictly typed). */ const anyObject = z.object({}).passthrough(); const anyResponse = z.unknown(); /** Placeholder for GET/DELETE endpoints that expect no request body. */ const noBody = z.object({}).passthrough(); function op(operation: ApiOperation): ApiOperation { return operation; } /** `?limit=&cursor=` → Convex `paginationOpts`. */ function paginationOptsFrom( input: Record, defaultLimit: number, ): { numItems: number; cursor: string | null } { const limit = typeof input.limit === "number" ? Math.min(Math.max(Math.floor(input.limit), 1), 100) : defaultLimit; return { numItems: limit, cursor: typeof input.cursor === "string" && input.cursor.length > 0 ? input.cursor : null, }; } function convex( functionType: ConvexOperation["functionType"], functionName: string, auth: ApiAuthMode = "bearer", options: Pick< ConvexOperation, "tokenArg" | "includeRunId" | "agentSessionAsApiKey" > = {}, ): ConvexOperation { return { kind: "convex", functionType, functionName, auth, tokenArg: options.tokenArg ?? (auth === "bearer" ? "sessionToken" : undefined), ...(options.agentSessionAsApiKey ? { agentSessionAsApiKey: true } : {}), includeRunId: options.includeRunId, }; } function gateway( method: GatewayOperation["method"], path: string, ): GatewayOperation { return { kind: "gateway", method, path, auth: "bearer" }; } function custom( handler: CustomOperation["handler"], auth: ApiAuthMode, ): CustomOperation { return { kind: "custom", handler, auth }; } function local(reason: string): LocalOnlyOperation { return { kind: "local", reason }; } const rawApiOperations = [ op({ id: "environments.list", group: "environments", summary: "List workspace-shared reusable setups and ready revisions (metadata only).", cli: "dench env list", method: "GET", path: "/environments", requestSchema: z .object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.string().optional(), }) .strict(), responseSchema: anyResponse, backend: convex("query", "functions/environments:list"), }), op({ id: "environments.get", group: "environments", summary: "Inspect a saved setup, its manifest, and an optional immutable revision.", cli: "dench env show", method: "GET", path: "/environments/{name}", requestSchema: z .object({ name: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/), revision: z .string() .regex(/^[a-f0-9]{64}$/) .optional(), }) .strict(), responseSchema: anyResponse, backend: convex("query", "functions/environments:get"), }), op({ id: "environments.upload", group: "environments", summary: "Reserve an owner-bound source upload. Prefix the uploaded bytes with the returned proof before saving.", cli: "dench env save", method: "POST", path: "/environments/uploads", requestSchema: z .object({ kind: z.literal("source"), size: z.number().int().min(1).max(16777216), sha256: z.string().regex(/^[a-f0-9]{64}$/), }) .strict(), responseSchema: anyResponse, backend: convex("mutation", "functions/environments:requestUpload"), }), op({ id: "environments.save", group: "environments", summary: "Publish an explicitly saved source and manifest for one-time preparation. Returns pending/building until verified; does not run the task.", cli: "dench env save", method: "POST", path: "/environments/{name}", requestSchema: z .object({ name: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/), manifest: z .object({ version: z.literal(1), name: z.string(), purpose: z.string(), files: z.array(z.string()), prepare: z.array(z.string()), verify: z.array(z.string()), env: z.record(z.string(), z.string()).optional(), secretEnv: z.record(z.string(), z.string()).optional(), secretFiles: z .array( z.object({ path: z.string(), secret: z.string(), mutable: z.boolean().optional(), }), ) .optional(), statePaths: z.array(z.string()).optional(), portable: z.boolean().optional(), }) .strict(), uploadId: z.string(), storageId: z.string(), sha256: z.string().regex(/^[a-f0-9]{64}$/), }) .strict(), responseSchema: anyResponse, backend: convex("action", "functions/environmentsNode:save"), }), op({ id: "environments.delete", group: "environments", summary: "Retire a saved setup from new selection; retain immutable revisions and active job evidence.", cli: "dench env delete", method: "DELETE", path: "/environments/{name}", requestSchema: z .object({ name: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/) }) .strict(), responseSchema: anyResponse, backend: convex("mutation", "functions/environments:remove"), }), op({ id: "environments.run", group: "environments", summary: "Run a command with prepared dependencies and declared credentials, saving declared state with a revision check.", cli: "dench env run", method: "POST", path: "/local/environments/run", requestSchema: noBody, responseSchema: anyResponse, backend: local( "Execution occurs in the CLI process. Use dench env run NAME -- COMMAND; secret-bearing execution resolution is not a general REST read endpoint.", ), }), op({ id: "secrets.list", group: "secrets", summary: "List workspace-shared credential names, kinds and versions; never values.", cli: "dench secrets list", method: "GET", path: "/secrets", requestSchema: z .object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.string().optional(), }) .strict(), responseSchema: anyResponse, backend: convex("query", "functions/workspaceSecrets:list"), }), op({ id: "secrets.set", group: "secrets", summary: "Encrypt a workspace credential. Use CLI stdin or the secure chat form; return metadata only. Do not put values in chat, manifests or scripts.", cli: "dench secrets set", method: "PUT", path: "/secrets/{name}", requestSchema: z .object({ name: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/), kind: z.enum(["env", "file"]), value: z.string().min(1).max(65536), expectedVersion: z.number().int().nonnegative().optional(), }) .strict(), responseSchema: z.object({ name: z.string(), kind: z.enum(["env", "file"]), version: z.number(), revision: z.number(), status: z.literal("stored"), }), backend: convex("action", "functions/workspaceSecretsNode:set"), }), op({ id: "secrets.delete", group: "secrets", summary: "Revoke a workspace credential for future resolution, optionally checking its version.", cli: "dench secrets delete", method: "DELETE", path: "/secrets/{name}", requestSchema: z .object({ name: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/), expectedVersion: z.number().int().nonnegative().optional(), }) .strict(), responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceSecrets:remove"), }), op({ id: "campaigns.drafts.create", group: "campaigns", summary: "Create an idempotent empty campaign draft with the caller's available sender pools. Does not launch or send.", cli: "dench campaign draft", method: "POST", path: "/campaigns/drafts", requestSchema: anyObject, responseSchema: anyResponse, // Built by commands, so opening it shows the campaign rather than the // app's New campaign flow. backend: { ...convex( "mutation", "functions/outreachSignalCampaigns:createPlainCampaign", ), fixedArgs: { resume: false }, }, }), op({ id: "campaigns.audience.search", group: "campaigns", summary: "Search people by filters without adding CRM records or campaign recipients. Search credits apply; this is not a free provider preview.", cli: "dench campaign search", method: "POST", path: "/campaigns/audience/search", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/outreachAudienceNode:search"), }), op({ id: "campaigns.people.addFromSearch", group: "campaigns", summary: "Search and add up to 50 people to an existing campaign. New identities become Leads. Search credits apply; use nextPageToken for another page.", cli: "dench campaign add --search", method: "POST", path: "/campaigns/{sequenceId}/people/search", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/outreachAudienceNode:addFromSearch"), }), op({ id: "campaigns.people.addBatch", group: "campaigns", summary: "Add up to 50 selected recipients without another search. Deduplicates, applies suppression, and preserves the campaign's launch state.", cli: "dench campaign add --people-file", method: "POST", path: "/campaigns/{sequenceId}/people/batch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:addEnrollments"), }), op({ id: "campaigns.sources.get", group: "campaigns", summary: "Read a campaign's saved signal source, targeting and schedule.", cli: "dench campaign sources get", method: "GET", path: "/campaigns/{sequenceId}/sources", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachLeadSources:getForSequence"), }), op({ id: "campaigns.sources.signals", group: "campaigns", summary: "Read signal choices, suggested terms and current source configuration.", cli: "dench campaign sources signals", method: "GET", path: "/campaigns/{sequenceId}/sources/signals", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachLeadSources:signalsOverview"), }), op({ id: "campaigns.sources.update", group: "campaigns", summary: "Update source cadence, pause/resume, targeting, signals or terms. Active sources may perform billable future work.", cli: "dench campaign sources set", method: "PATCH", path: "/campaigns/{sequenceId}/sources", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachLeadSources:update"), }), op({ id: "campaigns.sources.preview", group: "campaigns", summary: "Preview a signal source using its current audience. Provider credits may apply; does not enroll people.", cli: "dench campaign sources preview", method: "POST", path: "/campaigns/{sequenceId}/sources/preview", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/outreachLeadSourcesNode:previewSignal", ), }), op({ id: "campaigns.sources.run", group: "campaigns", summary: "Run a live campaign's signals now instead of waiting for the schedule (Run now). Provider credits apply; limited to 6 runs an hour.", cli: "dench campaign sources run", method: "POST", path: "/campaigns/{sequenceId}/sources/run", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachLeadSources:runNow"), }), op({ id: "campaigns.sources.overview", group: "campaigns", summary: "Read the Sources tab: each signal's status, last and next run, people found and anything needing attention.", cli: "dench campaign sources overview", method: "GET", path: "/campaigns/{sequenceId}/sources/overview", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachLeadSources:campaignOverview"), }), op({ id: "campaigns.sources.pending", group: "campaigns", summary: "List people a signal found who are waiting for approval (manual approval mode).", cli: "dench campaign sources pending", method: "GET", path: "/campaigns/{sequenceId}/sources/pending", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachLeadSources:listPendingReview"), }), op({ id: "campaigns.sources.review", group: "campaigns", summary: "Approve people waiting for review (they start the sequence) or reject them.", cli: "dench campaign sources approve|reject", method: "POST", path: "/campaigns/{sequenceId}/sources/review", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachLeadSources:reviewLeads"), }), op({ id: "campaigns.queue", group: "campaigns", summary: "Today's queue: who the campaign plans to contact today, warmest first, within the senders' limits.", cli: "dench campaign queue", method: "GET", path: "/campaigns/{sequenceId}/queue", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:getTodayQueue"), }), op({ id: "campaigns.builder.profile", group: "campaigns", summary: "Read the company profile and suggested campaign audience.", cli: "dench campaign builder profile", method: "GET", path: "/campaigns/builder/profile", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSignalCampaigns:getProfile"), }), op({ id: "campaigns.builder.save", group: "campaigns", summary: "Save a signal campaign's audience, signals and terms idempotently. Does not start a build.", cli: "dench campaign builder save", method: "POST", path: "/campaigns/builder/drafts", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSignalCampaigns:upsertDraft", ), }), op({ id: "campaigns.builder.get", group: "campaigns", summary: "Read signal build progress, preview people, messages and errors.", cli: "dench campaign builder get", method: "GET", path: "/campaigns/builder/drafts/{draftId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSignalCampaigns:getDraft"), }), op({ id: "campaigns.builder.build", group: "campaigns", summary: "Start the same billable signal/audience/message build used by the UI; poll the draft for progress.", cli: "dench campaign builder build", method: "POST", path: "/campaigns/builder/drafts/{draftId}/build", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("campaignBuilder", "bearer"), }), op({ id: "campaigns.builder.create", group: "campaigns", summary: "Create a campaign from a reviewed signal draft. Does not launch the campaign.", cli: "dench campaign builder create", method: "POST", path: "/campaigns/builder/drafts/{draftId}/create", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSignalCampaigns:create"), }), op({ id: "campaigns.builder.opener", group: "campaigns", summary: "Edit or reset one preview person's opening line before creating the campaign.", cli: "dench campaign builder opener", method: "PUT", path: "/campaigns/builder/drafts/{draftId}/people/{leadId}/opener", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSignalCampaigns:updateLeadOpener", ), }), op({ id: "campaigns.builder.drafts", group: "campaigns", summary: "List saved, unfinished campaign drafts that can be resumed.", cli: "dench campaign builder drafts", method: "GET", path: "/campaigns/builder/drafts", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "query", "functions/outreachSignalCampaigns:listSavedDrafts", ), }), op({ id: "campaigns.builder.resume", group: "campaigns", summary: "Read a saved draft's setup (source, name, targeting, signals, terms, lookalike) to resume it.", cli: "dench campaign builder resume", method: "GET", path: "/campaigns/builder/drafts/{draftId}/setup", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "query", "functions/outreachSignalCampaigns:getDraftResume", ), }), op({ id: "campaigns.builder.discard", group: "campaigns", summary: "Discard a saved draft that has not become a campaign.", cli: "dench campaign builder discard", method: "DELETE", path: "/campaigns/builder/drafts/{draftId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSignalCampaigns:discardDraft", ), }), op({ id: "campaigns.builder.outreach", group: "campaigns", summary: "Save a draft's Outreach and Review choices: AI or manual steps, the brief, the steps, who sends and when, and auto or manual approval.", cli: "dench campaign builder outreach", method: "PATCH", path: "/campaigns/builder/drafts/{draftId}/outreach", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSignalCampaigns:saveOutreach", ), }), op({ id: "campaigns.builder.sequence", group: "campaigns", summary: "Generate the draft's sequence from a brief (goal, problem, tone, channels), as Generate sequence does. AI credits apply.", cli: "dench campaign builder sequence", method: "POST", path: "/campaigns/builder/drafts/{draftId}/sequence", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("campaignBuilder", "bearer"), }), op({ id: "campaigns.builder.lookalike", group: "campaigns", summary: "Set up a lookalike from the best lead's LinkedIn URL (role, similar roles, place, industry, similar companies), suggest roles again, or re-scope to a company. Nothing is saved. Provider credits apply.", cli: "dench campaign builder lookalike", method: "POST", path: "/campaigns/builder/lookalike", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("campaignBuilder", "bearer"), }), op({ id: "campaigns.builder.voices", group: "campaigns", summary: "Suggest market voices (LinkedIn experts) for the campaign's topics, to watch with voice_engagement. AI credits apply.", cli: "dench campaign builder voices", method: "POST", path: "/campaigns/builder/voices", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("campaignBuilder", "bearer"), }), op({ id: "campaigns.builder.autofill", group: "campaigns", summary: "Turn an audience description into targeting filters. AI credits apply.", cli: "dench campaign builder autofill", method: "POST", path: "/campaigns/builder/autofill", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("campaignBuilder", "bearer"), }), op({ id: "campaigns.builder.estimate", group: "campaigns", summary: "Estimate audience or weekly signal volume using the UI's cached estimator. Provider credits may apply.", cli: "dench campaign builder estimate", method: "POST", path: "/campaigns/builder/estimate", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("campaignBuilder", "bearer"), }), op({ id: "commands.list", group: "meta", summary: "List all CLI/API operations and parity classifications.", cli: "dench help", method: "GET", path: "/commands", requestSchema: noBody, responseSchema: anyResponse, backend: { kind: "custom", handler: "commandsList", auth: "none" }, }), // Auth and local session management. op({ id: "auth.signin.create", group: "auth", summary: "Create an agent sign-in request.", cli: "dench signin", method: "POST", path: "/auth/signin-requests", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/agentWorkspace:createSignInRequest", "none", ), }), op({ id: "auth.signin.poll", group: "auth", summary: "Poll an agent sign-in request until approval.", cli: "dench signin", method: "POST", path: "/auth/signin-requests/{requestId}/poll", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/agentWorkspace:pollSignInRequest", "none", ), }), op({ id: "auth.otp.start", group: "auth", summary: "Start the email OTP sign-in flow.", cli: "dench signin --email", method: "POST", path: "/auth/otp/start", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("signinOtpStart", "none"), }), op({ id: "auth.otp.verify", group: "auth", summary: "Verify an email OTP sign-in code.", cli: "dench signin --email", method: "POST", path: "/auth/otp/verify", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("signinOtpVerify", "none"), }), op({ id: "auth.otp.finalize", group: "auth", summary: "Finalize an email OTP sign-in session.", cli: "dench signin --email", method: "POST", path: "/auth/otp/finalize", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("signinOtpFinalize", "none"), }), op({ id: "sessions.local.list", group: "local", summary: "List local CLI sessions.", cli: "dench sessions", method: "GET", path: "/local/sessions", requestSchema: noBody, responseSchema: anyResponse, backend: local("CLI sessions live in the caller's ~/.dench/config.json."), }), op({ id: "sessions.local.use", group: "local", summary: "Select a local CLI session.", cli: "dench use ", method: "POST", path: "/local/sessions/use", requestSchema: anyObject, responseSchema: anyResponse, backend: local("The server cannot mutate the caller's local CLI config."), }), op({ id: "sessions.local.logout", group: "local", summary: "Remove local CLI sessions.", cli: "dench logout", method: "POST", path: "/local/logout", requestSchema: anyObject, responseSchema: anyResponse, backend: local("The server cannot mutate the caller's local CLI config."), }), op({ id: "backend.local.configure", group: "local", summary: "Configure the CLI backend target.", cli: "dench backend show|set|clear", method: "POST", path: "/local/backend", requestSchema: anyObject, responseSchema: anyResponse, backend: local("Backend selection is local CLI state."), }), op({ id: "workspace.context.sections", group: "workspace", summary: "Read selected scoped workspace documents and a compact context index without writes.", cli: "dench context --sections", method: "GET", path: "/context/sections", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:workspaceContext"), }), // Workspace orientation. op({ id: "workspace.context", group: "workspace", summary: "Return the current Dench workspace context for the bearer.", cli: "dench context", method: "GET", path: "/context", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:whatCanIDoHere"), }), op({ id: "workspace.instructions", group: "workspace", summary: "Return the current workspace's standing instructions for external agents.", cli: "dench context --instructions", method: "GET", path: "/context/instructions", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:workspaceInstructions"), }), op({ id: "workspace.status", group: "workspace", summary: "Return workspace and agent status.", cli: "dench status", method: "GET", path: "/status", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:whatCanIDoHere"), }), op({ id: "workspace.agents", group: "workspace", summary: "List agents visible in the workspace status payload.", cli: "dench agents", method: "GET", path: "/agents", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:whatCanIDoHere"), }), op({ id: "workspace.approvals.list", group: "approvals", summary: "List approval requests visible in the workspace status payload.", cli: "dench approvals", method: "GET", path: "/approvals", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:whatCanIDoHere"), }), op({ id: "workspace.artifacts.list", group: "workspace", summary: "List artifacts for the authenticated workspace.", cli: "dench artifacts", method: "GET", path: "/artifacts", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:agentListArtifacts"), }), op({ id: "workspace.suggestedWork.list", group: "workspace", summary: "List draft suggested-work artifacts.", cli: "dench suggested-work", method: "GET", path: "/suggested-work", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:agentListArtifacts"), }), // Lists (saved searches). Find results persist as named list artifacts; // these ops give agents the same full management the sidebar has. // CRM lists (saved views) are managed through the `crm.views.*` ops. op({ id: "lists.list", group: "lists", summary: "List saved searches (find-result lists) in the workspace, newest first.", cli: "dench lists", aliases: ["dench lists list"], method: "GET", path: "/lists", requestSchema: z.object({ search: z.string().optional(), cursor: z.string().optional(), limit: z.coerce.number().int().min(1).max(100).optional(), }), responseSchema: anyResponse, backend: { ...convex("query", "functions/workspaceArtifacts:listSaved"), transformArgs: (input) => ({ search: input.search, paginationOpts: paginationOptsFrom(input, 25), }), }, }), op({ id: "lists.rows", group: "lists", summary: "Read saved rows with current enrichment results and original indices; cursor-paged, without repeating a search.", cli: "dench lists rows", method: "GET", path: "/lists/{artifactId}/rows", requestSchema: z.object({ artifactId: z.string().min(1), cursor: z.string().optional(), limit: z.coerce.number().int().min(1).max(100).optional(), enrichment: z .enum([ "all", "enriched", "in_progress", "no_contacts", "failed", "not_enriched", ]) .optional(), }), responseSchema: anyResponse, backend: convex("action", "functions/workspaceArtifacts:listRows"), }), op({ id: "lists.enrich", group: "lists", summary: "Buy contact details for 1–100 saved profiles and save them to the list. Skips fields already found, looked up empty, or in flight. Requires a signed-in member or their personal agent.", cli: "dench lists enrich", method: "POST", path: "/lists/{artifactId}/enrich", requestSchema: z.object({ artifactId: z.string().min(1), rows: z.array(z.record(z.string(), z.unknown())).min(1).max(100), fields: z .array(z.enum(["email", "phone", "personal_email"])) .min(1) .describe("email is work email."), }), responseSchema: anyResponse, backend: convex("action", "functions/workspaceArtifacts:enrichSelected"), }), op({ id: "lists.import", group: "lists", summary: "Explicitly import selected saved profiles, optionally filtered by enrichment, with a configured Lead status. Reuses CRM records; never sends outreach or triggers conversion.", cli: "dench artifacts import", method: "POST", path: "/lists/{artifactId}/import", requestSchema: z.object({ artifactId: z.string().min(1), to: z.enum(["lead", "company", "job_posting"]), rowIndexes: z.array(z.number().int().min(0)).optional(), status: z.string().min(1).optional(), enrichment: z .enum([ "all", "enriched", "in_progress", "no_contacts", "failed", "not_enriched", ]) .optional(), viewName: z.string().optional(), pin: z.boolean().optional(), }), responseSchema: anyResponse, backend: convex("action", "functions/workspaceArtifacts:importArtifact"), }), op({ id: "lists.get", group: "lists", summary: "Get one saved search / list by artifact id.", cli: "dench lists show", method: "GET", path: "/lists/{artifactId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/workspaceArtifacts:get"), }), op({ id: "lists.save", group: "lists", summary: "Save find results as a named list (rows + filters become a workspace artifact).", cli: "dench lists save", aliases: ['dench leads find --save-as "Name"'], method: "POST", path: "/lists", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/workspaceArtifacts:saveFind"), }), op({ id: "lists.update", group: "lists", summary: "Update a saved list in place (title, filters, and/or replacement rows).", cli: "dench lists update", method: "PATCH", path: "/lists/{artifactId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/workspaceArtifacts:updateFind"), }), op({ id: "lists.rename", group: "lists", summary: "Rename a saved search / list.", cli: "dench lists rename", method: "POST", path: "/lists/{artifactId}/rename", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceArtifacts:rename"), }), op({ id: "lists.delete", group: "lists", summary: "Delete a saved search / list, its exported CSV/JSONL files, and any linked watch.", cli: "dench lists delete", method: "DELETE", path: "/lists/{artifactId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceArtifacts:remove"), }), op({ id: "lists.prospects.reviews", group: "lists", summary: "Read durable enrichment activity (legacy review metadata remains readable). Use lists rows for current saved profiles and contacts.", cli: "dench lists reviews", method: "GET", path: "/lists/{artifactId}/reviews", requestSchema: z.object({ artifactId: z.string().min(1), limit: z.coerce.number().int().min(1).max(100).optional(), cursor: z.string().optional(), }), responseSchema: anyResponse, backend: { ...convex("query", "functions/prospectReviews:list"), transformArgs: (input) => ({ artifactId: input.artifactId, paginationOpts: paginationOptsFrom(input, 50), }), }, }), op({ id: "lists.prospects.assess", group: "lists", summary: "Suggest ICP fit for one profile. Never changes a human review decision.", cli: "dench lists assess", method: "POST", path: "/lists/{artifactId}/assess", requestSchema: z.object({ artifactId: z.string().min(1), row: z.record(z.string(), z.unknown()), suggestion: z.object({ fit: z.enum(["likely_fit", "unlikely_fit", "unknown"]), score: z .number() .int() .min(0) .max(100) .optional() .describe( "AI relevance to the saved ICP, not conversion probability.", ), reason: z.string().min(1).max(2000), }), }), responseSchema: anyResponse, backend: convex("mutation", "functions/prospectReviews:assess"), }), // Memory and approvals. op({ id: "memory.search", group: "memory", summary: "Search durable workspace memory.", cli: "dench memory search", method: "GET", path: "/memory/search", requestSchema: z .object({ query: z.string(), limit: z.coerce.number().optional() }) .passthrough(), responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:agentSearchMemory"), }), op({ id: "memory.save", group: "memory", summary: "Save a durable workspace memory fact.", cli: "dench memory save", method: "POST", path: "/memory", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentWorkspace:agentSaveMemory"), }), op({ id: "approval.request", group: "approvals", summary: "Request human approval.", cli: "dench approval request", method: "POST", path: "/approvals", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/agentWorkspace:agentRequestApproval", ), }), op({ id: "approval.decide", group: "approvals", summary: "Record an approval decision.", cli: "dench approval approve|reject", method: "POST", path: "/approvals/{approvalId}/decision", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentWorkspace:agentDecideApproval"), }), // Billing. op({ id: "billing.status", group: "billing", summary: "Get billing and AI credit status.", cli: "dench billing status", method: "GET", path: "/billing/status", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentWorkspace:getAgentBillingStatus"), }), op({ id: "billing.topup", group: "billing", summary: "Create an AI credit top-up checkout link.", cli: "dench billing topup", method: "POST", path: "/billing/topup", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("billingTopup", "bearer"), }), op({ id: "billing.upgrade", group: "billing", summary: "Create a workspace upgrade checkout link.", cli: "dench upgrade", method: "POST", path: "/billing/upgrade", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("upgrade", "bearer"), }), // Chat and agent aliases. op({ id: "chat.list", group: "chat", summary: "List past chat threads.", cli: "dench chat list", method: "GET", path: "/chat/threads", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/chat:listPastChats", "bearer", { includeRunId: true, }), }), op({ id: "chat.search", group: "chat", summary: "Search past chat messages.", cli: "dench chat search", method: "GET", path: "/chat/search", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/chat:searchPastChatsComplete", "bearer", { includeRunId: true, }, ), }), op({ id: "chat.read", group: "chat", summary: "Read a past chat thread.", cli: "dench chat read", method: "GET", path: "/chat/threads/{threadId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/chat:readPastChat", "bearer", { includeRunId: true, }), }), op({ id: "chat.tree", group: "chat", summary: "Read the parent-child chat thread hierarchy.", cli: "dench chat tree", method: "GET", path: "/chat/tree", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/chat:listPastChats", "bearer", { includeRunId: true, }), }), op({ id: "chat.rename", group: "chat", summary: "Rename one or more chat threads.", cli: "dench chat rename", method: "PATCH", path: "/chat/threads", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/chat:renameThreads", "bearer", { includeRunId: true, }), }), op({ id: "chat.delete", group: "chat", summary: "Delete one or more chat threads.", cli: "dench chat delete", method: "DELETE", path: "/chat/threads", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/chat:deleteThreads", "bearer", { includeRunId: true, }), }), op({ id: "chat.new", group: "chat", summary: "Start a new chat-turn workflow.", cli: "dench chat new", aliases: ["dench agent new"], method: "POST", path: "/chat/threads", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("chatSpawn", "bearer"), }), op({ id: "chat.send", group: "chat", summary: "Send a message to an existing chat thread.", cli: "dench chat send", aliases: ["dench agent send"], method: "POST", path: "/chat/threads/{threadId}/messages", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("chatSpawn", "bearer"), }), op({ id: "chat.follow", group: "chat", summary: "Follow a live chat response stream.", cli: "dench chat follow", aliases: ["dench agent follow"], method: "GET", path: "/chat/threads/{threadId}/stream", requestSchema: noBody, responseSchema: anyResponse, backend: custom("chatFollow", "bearer"), }), // CRM core. op({ id: "crm.objects.list", group: "crm", summary: "List CRM objects.", cli: "dench crm objects list [--with-references]", method: "GET", path: "/crm/objects", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/objects:list"), }), op({ id: "crm.objects.listCompact", group: "crm", summary: "List names, display labels, visibility and workspace counts without internal state.", cli: "dench crm objects list --compact", method: "GET", path: "/crm/catalog/objects", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/objects:listCompact"), }), op({ id: "crm.objects.get", group: "crm", summary: "Get a CRM object.", cli: "dench crm objects get", method: "GET", path: "/crm/objects/{name}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/objects:get"), }), op({ id: "crm.objects.create", group: "crm", summary: "Create a CRM object.", cli: "dench crm objects create", method: "POST", path: "/crm/objects", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:create"), }), op({ id: "crm.objects.update", group: "crm", summary: "Update a CRM object.", cli: "dench crm objects update", method: "PATCH", path: "/crm/objects/{name}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:update"), }), op({ id: "crm.objects.rename", group: "crm", summary: "Rename a CRM object.", cli: "dench crm objects rename", method: "POST", path: "/crm/objects/{from}/rename", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:rename"), }), op({ id: "crm.objects.delete", group: "crm", summary: "Delete a CRM object.", cli: "dench crm objects delete", method: "DELETE", path: "/crm/objects/{name}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:remove"), }), op({ id: "crm.objects.archive", group: "crm", summary: "Hide a CRM object's workspace sidebar row for the whole workspace. Not a delete — the table keeps working. Admin-only.", cli: "dench crm objects archive", method: "POST", path: "/crm/objects/{name}/archive", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:setArchived"), }), op({ id: "crm.objects.unarchive", group: "crm", summary: "Restore a CRM object's workspace sidebar row. Admin-only.", cli: "dench crm objects unarchive", method: "POST", path: "/crm/objects/{name}/unarchive", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:setArchived"), }), op({ id: "crm.views.list", group: "crm", summary: "List the saved views on a CRM object (plus the active view).", cli: "dench crm views list", method: "GET", path: "/crm/objects/{objectName}/views", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/objects:listViews"), }), op({ id: "crm.views.get", group: "crm", summary: "Get one saved view's definition by name.", cli: "dench crm views get", method: "GET", path: "/crm/objects/{objectName}/views/{viewName}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/objects:getView"), }), op({ id: "crm.views.entries", group: "crm", summary: "Materialize a saved view's rows (applies its filters/sort/pins), paginated.", cli: "dench crm views entries", method: "GET", path: "/crm/objects/{objectName}/views/{viewName}/entries", requestSchema: noBody, responseSchema: anyResponse, backend: convex("action", "functions/crm/objects:completeView"), }), op({ id: "crm.views.create", group: "crm", summary: "Create a saved view (named filters/sort/columns combo) on a CRM object.", cli: "dench crm views create", method: "POST", path: "/crm/objects/{objectName}/views", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:saveView"), }), op({ id: "crm.views.update", group: "crm", summary: "Replace a saved view's definition (upsert by name).", cli: "dench crm views update", method: "PATCH", path: "/crm/objects/{objectName}/views/{viewName}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:saveView"), }), op({ id: "crm.views.rename", group: "crm", summary: "Rename a saved view in place — keeps its filters, pins, and sidebar slot.", cli: "dench crm views rename", method: "POST", path: "/crm/objects/{objectName}/views/{viewName}/rename", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:renameView"), }), op({ id: "crm.views.delete", group: "crm", summary: "Delete a saved view by name (idempotent).", cli: "dench crm views delete", method: "DELETE", path: "/crm/objects/{objectName}/views/{viewName}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:deleteView"), }), op({ id: "crm.views.archive", group: "crm", summary: "Move a pinned view's sidebar row into the Archived flyout. Admin-only.", cli: "dench crm views archive", method: "POST", path: "/crm/objects/{objectName}/views/{viewName}/archive", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:setArchived"), }), op({ id: "crm.views.unarchive", group: "crm", summary: "Restore a view's sidebar row (re-asserts its pin). Admin-only.", cli: "dench crm views unarchive", method: "POST", path: "/crm/objects/{objectName}/views/{viewName}/unarchive", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/objects:setArchived"), }), // The workspace app rail's Library. The whole layout — row order AND // named groups — is WORKSPACE-SHARED (one tree for every member, // stored on the organization). Reordering (`move`) and archiving are // admin-only; group management and view pinning are open to any member. op({ id: "crm.sidebar.list", group: "crm", summary: "Describe the workspace sidebar: Library rows in tree order (with their groups), the home-level Pinned strip, what is hidden and why, and whether the caller is an admin.", cli: "dench crm sidebar list", method: "GET", path: "/crm/sidebar", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/workspaceNav:describeMySidebar"), }), op({ id: "crm.sidebar.move", group: "crm", summary: "Reposition one Library row (or group) among the Library's top-level slots. Does not add a home-level Pinned tab. Workspace-shared; admin-only.", cli: "dench crm sidebar move", method: "POST", path: "/crm/sidebar/move", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceNav:repositionMyNavItem"), }), op({ id: "crm.sidebar.groups.create", group: "crm", summary: 'Create a named sidebar group (e.g. "Hiring") in the workspace-shared Library. Open to any member.', cli: "dench crm sidebar group create", method: "POST", path: "/crm/sidebar/groups", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceNav:createNavGroup"), }), op({ id: "crm.sidebar.groups.rename", group: "crm", summary: "Rename a sidebar group. Open to any member.", cli: "dench crm sidebar group rename", method: "POST", path: "/crm/sidebar/groups/rename", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceNav:renameNavGroup"), }), op({ id: "crm.sidebar.groups.delete", group: "crm", summary: "Dissolve a sidebar group; its rows return to the top level in its slot. Open to any member.", cli: "dench crm sidebar group delete", method: "POST", path: "/crm/sidebar/groups/delete", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceNav:deleteNavGroup"), }), op({ id: "crm.sidebar.groups.move-item", group: "crm", summary: "Move a sidebar row into a group (groupId set) or back to the top level (groupId null). Open to any member.", cli: "dench crm sidebar group add", aliases: ["dench crm sidebar group remove"], method: "POST", path: "/crm/sidebar/groups/move", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceNav:moveNavItemToGroup"), }), op({ id: "crm.sidebar.pins.set", group: "crm", summary: 'Pin (pinned true) or unpin a Library row — or a whole group — on the home-level Pinned shortcut strip (the "tab on the left"). Does not move the row inside Library. Workspace-shared; open to any member.', cli: "dench crm sidebar pin", aliases: ["dench crm sidebar unpin"], method: "POST", path: "/crm/sidebar/pins", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/workspaceNav:setNavRootPin"), }), op({ id: "crm.fields.list", group: "crm", summary: "List fields for a CRM object, including read-only calculation metadata.", cli: "dench crm fields list", method: "GET", path: "/crm/objects/{objectName}/fields", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/fields:list"), }), op({ id: "crm.fields.listCompact", group: "crm", summary: "List field names, types and effective constraints without indexing or enrichment internals.", cli: "dench crm fields list --compact", method: "GET", path: "/crm/catalog/objects/{objectName}/fields", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/fields:listCompact"), }), op({ id: "crm.fields.create", group: "crm", summary: "Create a CRM field or read-only campaign activity boolean column.", cli: "dench crm fields create", method: "POST", path: "/crm/objects/{objectName}/fields", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:create"), }), op({ id: "crm.fields.update", group: "crm", summary: "Update a CRM field or change/explicitly clear an existing campaign calculation.", cli: "dench crm fields update", method: "PATCH", path: "/crm/objects/{objectName}/fields/{fieldName}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:update"), }), op({ id: "crm.fields.delete", group: "crm", summary: "Delete a CRM field.", cli: "dench crm fields delete", method: "DELETE", path: "/crm/objects/{objectName}/fields/{fieldName}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:remove"), }), op({ id: "crm.fields.reorder", group: "crm", summary: "Reorder CRM fields.", cli: "dench crm fields reorder", method: "POST", path: "/crm/objects/{objectName}/fields/reorder", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:reorder"), }), op({ id: "crm.fields.enum.reorder", group: "crm", summary: "Reorder enum values.", cli: "dench crm fields enum-reorder", method: "POST", path: "/crm/objects/{objectName}/fields/{fieldName}/enum/reorder", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:reorderEnumValues"), }), op({ id: "crm.fields.enum.add", group: "crm", summary: "Add an enum value.", cli: "dench crm fields enum-add", method: "POST", path: "/crm/objects/{objectName}/fields/{fieldName}/enum", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:addEnumValue"), }), op({ id: "crm.fields.enum.remove", group: "crm", summary: "Remove an enum value.", cli: "dench crm fields enum-remove", method: "DELETE", path: "/crm/objects/{objectName}/fields/{fieldName}/enum/{value}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:removeEnumValue"), }), op({ id: "crm.fields.enum.rename", group: "crm", summary: "Rename an enum value.", cli: "dench crm fields enum-rename", method: "POST", path: "/crm/objects/{objectName}/fields/{fieldName}/enum/{oldValue}/rename", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:renameEnumValue"), }), op({ id: "crm.fields.enum.color", group: "crm", summary: "Set an enum color.", cli: "dench crm fields enum-set-color", method: "POST", path: "/crm/objects/{objectName}/fields/{fieldName}/enum/{value}/color", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/fields:setEnumColor"), }), op({ id: "crm.entries.list", group: "crm", summary: "List CRM entries.", cli: "dench crm entries list [--with-references]", method: "GET", path: "/crm/objects/{objectName}/entries", requestSchema: z .object({ withReferences: z.boolean().optional() }) .passthrough(), responseSchema: anyResponse, backend: convex("query", "functions/crm/entries:list"), }), op({ id: "crm.entries.get", group: "crm", summary: "Get a CRM entry.", cli: "dench crm entries get", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("action", "functions/crm/recordRead:getEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, ...(input.withContext === true || input.withContext === "true" ? { withContext: true } : {}), }), }, }), op({ id: "crm.entries.syncHistory", group: "crm", summary: "Queue a full, paged pull of the record's email history from every connected mailbox; the ledger fills as pages land.", cli: "dench crm entries sync-history", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/sync-history", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/emailIdentityHistory:requestForEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, }), }, }), op({ id: "crm.entries.syncHistoryStatus", group: "crm", summary: "Has Dench read this record's email history? One row per address per mailbox.", cli: "dench crm entries sync-history --status", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/sync-history", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/emailIdentityHistory:statusForEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, }), }, }), op({ id: "crm.entries.dossier.get", group: "crm", summary: "Status of the account's research dossier — the delimited `dench:dossier` section Dench keeps in the record's Notes: status, when it was written, how many dated updates followed, the last error.", cli: "dench crm entries dossier", method: "GET", path: "/crm/entries/{entryId}/dossier", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/dossier:getForEntry"), transformArgs: (input) => ({ entryId: input.entryId }), }, }), op({ id: "crm.entries.dossier.regenerate", group: "crm", summary: "Research the account again and append a fresh dossier section to its Notes (needs a Website / Domain on the record).", cli: "dench crm entries dossier --regenerate", method: "POST", path: "/crm/entries/{entryId}/dossier/regenerate", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/dossier:regenerate"), transformArgs: (input) => ({ entryId: input.entryId }), }, }), // Per-record privacy (shared-by-default, 2026-09-25): the caller's own // opt-out — "hide MY connections' threads about this record from the // workspace". An account covers its contacts and opportunities. CRM read // access is enough; it only narrows the caller's own rows. op({ id: "crm.privacy.get", group: "crm", summary: "Whether the caller hides their own emails / messages with this record from the workspace (directly, or through its account), and whether they own a connection the toggle applies to.", cli: "dench crm privacy status", method: "GET", path: "/crm/entries/{entryId}/privacy", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/recordPrivacy:getRecordPrivacy"), transformArgs: (input) => ({ entryId: input.entryId }), }, }), op({ id: "crm.privacy.set", group: "crm", summary: "Hide (`hidden: true`) or show the caller's own emails / messages with this record to the workspace. An account covers every contact and opportunity linked to it; existing ledger rows are restamped and the shared Brief regenerates.", cli: "dench crm privacy hide|show", method: "PUT", path: "/crm/entries/{entryId}/privacy", requestSchema: z.object({ entryId: z.string(), hidden: z.boolean() }), responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/recordPrivacy:setRecordPrivacy"), transformArgs: (input) => ({ entryId: input.entryId, hidden: input.hidden === true, }), }, }), // Account detection (plan §4): the verdicts Dench took on counterparties // that emailed the workspace, the LinkedIn resolver's rows on the same // table, and the ONE rollback for everything the ingestion jobs created // (§13 #38): detections (email + sweep), linkedin, email-tasks. op({ id: "crm.detections.list", group: "crm", summary: "List account-detection verdicts — why a domain (or personal address) did or did not become an account: verdict, counterparty type, probabilities, evidence threads, the rows created. `kind` picks the creator: `detections` (email + sweep), `linkedin` (the LinkedIn resolver's rows) or `all` (default). Newest first.", cli: "dench crm detections list", method: "GET", path: "/crm/detections", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/accountDetection:list"), transformArgs: (input) => ({ ...(typeof input.verdict === "string" ? { verdict: input.verdict } : {}), ...(input.kind === "detections" || input.kind === "linkedin" || input.kind === "all" ? { kind: input.kind } : {}), ...(typeof input.since === "number" ? { since: input.since } : {}), ...(typeof input.limit === "number" ? { limit: Math.min(Math.max(Math.floor(input.limit), 1), 200) } : {}), }), }, }), op({ id: "crm.detections.undo", group: "crm", summary: "Undo what the ingestion jobs created since a timestamp (admin): `kind` = `detections` (accounts + contacts from the email detector and the sweep), `linkedin` (the LinkedIn resolver's contacts + accounts; their conversations are unlinked and parked), `email-tasks` (auto-created tasks and cases) or `all` (default). Deletes only rows no human has touched and marks the verdicts / decisions rolled_back so they are never re-created. `dryRun` lists what would go; per-kind counts in `kinds`.", cli: "dench crm detections undo", method: "POST", path: "/crm/detections/undo", requestSchema: z.object({ since: z .number() .describe("Only verdicts / decisions at or after this ms timestamp."), kind: z .enum(["detections", "linkedin", "email-tasks", "all"]) .optional() .describe("Which creator's rows to undo. Default `all`."), dryRun: z.boolean().optional(), limit: z.number().int().min(1).max(50).optional(), }), responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/accountDetection:undo"), transformArgs: (input) => ({ since: input.since, ...(typeof input.kind === "string" ? { kind: input.kind } : {}), ...(input.dryRun === true ? { dryRun: true } : {}), ...(typeof input.limit === "number" ? { limit: input.limit } : {}), }), }, }), op({ id: "crm.entries.history", group: "crm", summary: "List a CRM entry's audit history.", cli: "dench crm history", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/history", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/history:listForEntry"), transformArgs: (input) => { const limit = typeof input.limit === "number" ? Math.min(Math.max(Math.floor(input.limit), 1), 100) : 50; return { objectName: input.objectName, entryId: input.entryId, includeSystem: input.includeSystem === true, paginationOpts: { numItems: limit, cursor: typeof input.cursor === "string" && input.cursor.length > 0 ? input.cursor : null, }, }; }, }, }), // CRM activity ledger — one timeline per record: notes, comments, field // changes and every external touch (email, calendar, meeting, Slack, // LinkedIn). Newest first by default; only rows the caller may read. op({ id: "crm.activity.list", group: "crm", summary: "List a CRM entry's activity ledger (comments, notes, field changes, emails, meetings, Slack, LinkedIn) in one timeline, newest first by default.", cli: "dench crm activity", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/activity", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/activity:listForEntry"), transformArgs: (input) => { const limit = typeof input.limit === "number" ? Math.min(Math.max(Math.floor(input.limit), 1), 200) : 40; const kinds = typeof input.kinds === "string" ? input.kinds .split(",") .map((k: string) => k.trim()) .filter(Boolean) : Array.isArray(input.kinds) ? input.kinds : undefined; return { objectName: input.objectName, entryId: input.entryId, order: input.order === "asc" ? "asc" : "desc", limit, includeSystem: input.includeSystem === true, ...(kinds && kinds.length > 0 ? { kinds } : {}), ...(typeof input.before === "number" ? { before: input.before } : {}), ...(typeof input.after === "number" ? { after: input.after } : {}), }; }, }, }), op({ id: "crm.brief.get", group: "crm", summary: "The record's generated Brief: health, who owes the next move, open threads, commitments, risks, next step. Read this before the ledger.", cli: "dench crm brief", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/brief", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("action", "functions/crm/recordRead:getBrief"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, ...(input.includeContext === true || input.includeContext === "true" ? { includeContext: true } : {}), }), }, }), op({ id: "crm.context.get", group: "crm", summary: "Read-only source diagnostics: accessible evidence, association reasons, last Brief source manifest, generation queue, and your mailbox delivery health. Bounded, cursor-paged snapshot; does not certify complete history.", cli: "dench crm context", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/context", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/contextDiagnostics:getForEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, ...(typeof input.cursor === "string" ? { cursor: input.cursor } : {}), }), }, }), op({ id: "crm.brief.regenerate", group: "crm", summary: "Rebuild a record's Brief from the current timeline.", cli: "dench crm brief --regenerate", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/brief/regenerate", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("action", "functions/crm/recordRead:regenerateBrief"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, }), }, }), op({ id: "crm.activity.count", group: "crm", summary: "Bounded per-source counts of a CRM entry's activity ledger.", cli: "dench crm activity", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/activity/count", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/activity:countForEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, includeSystem: input.includeSystem === true, }), }, }), op({ id: "crm.notes.list", group: "crm", summary: "List a CRM entry's notes (titled, document-style comments), newest first.", cli: "dench crm notes list", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/notes", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/activity:listNotesForEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, limit: typeof input.limit === "number" ? Math.min(Math.max(Math.floor(input.limit), 1), 200) : 50, }), }, }), op({ id: "crm.notes.create", group: "crm", summary: "Add a titled note to a CRM entry's activity timeline (markdown body). Research goes here, never into the Notes document.", cli: "dench crm notes create", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/notes", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex( "action", "functions/crm/comments:createAndDispatch", "bearer", { includeRunId: true, }, ), transformArgs: (input) => ({ ...input, kind: "note" }), }, }), // CRM comments — threaded conversation on a record, merged with history // into the record's Activity feed. `@Dench` in a body summons the agent. op({ id: "crm.comments.list", group: "crm", summary: "List comments on a CRM entry (roots newest first, with preview replies).", cli: "dench crm comments list", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/comments", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/comments:listForEntry"), transformArgs: (input) => ({ objectName: input.objectName, entryId: input.entryId, previewReplies: typeof input.replies === "number" ? Math.min(Math.max(Math.floor(input.replies), 0), 50) : undefined, paginationOpts: paginationOptsFrom(input, 20), }), }, }), op({ id: "crm.comments.replies", group: "crm", summary: "List every reply under a root comment, oldest first.", cli: "dench crm comments replies", method: "GET", path: "/crm/comments/{commentId}/replies", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/comments:listReplies"), transformArgs: (input) => ({ rootCommentId: input.commentId, paginationOpts: paginationOptsFrom(input, 50), }), }, }), op({ id: "crm.comments.get", group: "crm", summary: "Get one comment with its entry label.", cli: "dench crm comments get", method: "GET", path: "/crm/comments/{commentId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/comments:get"), }), op({ id: "crm.comments.create", group: "crm", summary: "Comment on a CRM entry (markdown). Mention members with [@Name](/?member=); mention Dench with [@Dench](/?member=dench) or mentionDench.", cli: "dench crm comments create", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/comments", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/crm/comments:createAndDispatch", "bearer", { includeRunId: true, }, ), }), op({ id: "crm.comments.update", group: "crm", summary: "Edit a comment you wrote.", cli: "dench crm comments update", method: "PATCH", path: "/crm/comments/{commentId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/comments:edit"), }), op({ id: "crm.comments.delete", group: "crm", summary: "Delete a comment (kept as a tombstone when it has replies).", cli: "dench crm comments delete", method: "DELETE", path: "/crm/comments/{commentId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/comments:remove"), }), op({ id: "crm.comments.react", group: "crm", summary: "Toggle an emoji reaction on a comment.", cli: "dench crm comments react", method: "PUT", path: "/crm/comments/{commentId}/reactions/{emoji}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/comments:toggleReaction"), }), op({ id: "crm.watch.get", group: "crm", summary: "Whether you follow a CRM entry's activity.", cli: "dench crm watch status", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/watch", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/crm/comments:getWatchState"), transformArgs: (input) => ({ entryId: input.entryId }), }, }), op({ id: "crm.watch.set", group: "crm", summary: "Follow or unfollow a CRM entry's activity.", cli: "dench crm watch", method: "PUT", path: "/crm/objects/{objectName}/entries/{entryId}/watch", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/comments:setWatching"), transformArgs: (input) => ({ entryId: input.entryId, watching: input.watching !== false, }), }, }), // CRM tasks — conveniences over the protected `task` object. Tasks are // still ordinary entries (`crm.entries.*` works on them); these add the // "mine", "for this record", complete/reopen shortcuts the app uses. op({ id: "crm.tasks.pageAssigned", group: "crm", summary: "Page assigned tasks in priority order (or completed tasks by recency).", cli: "dench crm tasks mine --page", method: "POST", path: "/crm/tasks/assigned/page", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/tasks:pageAssignedTasks"), }), op({ id: "crm.tasks.pageForEntry", group: "crm", summary: "Page all tasks linked to a record, including subtasks.", cli: "dench crm tasks list --page", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/tasks/page", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/tasks:pageEntryTasks"), }), op({ id: "crm.tasks.prepare", group: "crm", summary: "Start or resume preparation of exact task ordering and progress.", cli: "dench crm tasks prepare", method: "POST", path: "/crm/tasks/prepare", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/taskReadModels:ensureReady"), }), op({ id: "crm.tasks.mine", group: "crm", summary: "Your open tasks (plus ones completed today).", cli: "dench crm tasks mine", method: "GET", path: "/crm/tasks/mine", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/tasks:listMyOpenTasks"), }), op({ id: "crm.tasks.forEntry", group: "crm", summary: "Tasks linked to a CRM entry.", cli: "dench crm tasks list", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/tasks", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/tasks:listEntryTasks"), }), op({ id: "crm.tasks.createForEntry", group: "crm", summary: "Create a task linked to a CRM entry.", cli: "dench crm tasks create", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/tasks", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/tasks:createEntryTask"), }), op({ id: "crm.tasks.complete", group: "crm", summary: "Mark a task done.", cli: "dench crm tasks complete", method: "POST", path: "/crm/tasks/{entryId}/complete", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/tasks:completeTask"), }), op({ id: "crm.tasks.reopen", group: "crm", summary: "Reopen a completed task.", cli: "dench crm tasks reopen", method: "POST", path: "/crm/tasks/{entryId}/reopen", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/tasks:reopenTask"), }), op({ id: "crm.tasks.setPriority", group: "crm", summary: "Set or clear a task's priority.", cli: "dench crm tasks priority", method: "PUT", path: "/crm/tasks/{entryId}/priority", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/tasks:setTaskPriority"), }), // Subtasks are ordinary task rows whose `Parent Task` self-relation points // at another task (one level deep). The field is created on first use. op({ id: "crm.tasks.subtasks.list", group: "crm", summary: "A task's subtasks.", cli: "dench crm tasks subtasks", method: "GET", path: "/crm/tasks/{entryId}/subtasks", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/tasks:listSubtasks"), }), op({ id: "crm.tasks.subtasks.create", group: "crm", summary: "Create a subtask under a task (inherits the parent's record relations).", cli: "dench crm tasks create-subtask", method: "POST", path: "/crm/tasks/{entryId}/subtasks", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/tasks:createSubtask"), transformArgs: (input) => { const { entryId, ...rest } = input; return { ...rest, parentEntryId: entryId }; }, }, }), op({ id: "crm.tasks.setParent", group: "crm", summary: "Nest a task under a parent, or promote it (parentEntryId: null).", cli: "dench crm tasks parent", method: "PUT", path: "/crm/tasks/{entryId}/parent", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/tasks:setTaskParent"), }), op({ id: "crm.tasks.delete", group: "crm", summary: "Delete a task and its subtasks, or keep them as tasks.", cli: "dench crm tasks delete", method: "DELETE", path: "/crm/tasks/{entryId}", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("action", "functions/crm/tasks:deleteTaskComplete"), transformArgs: (input) => ({ entryId: input.entryId, subtasks: input.subtasks === "keep" ? "keep" : "delete", }), }, }), // Repeating tasks — a property of the task's Due Date. One series per task, // a new task per occurrence (linked through the hidden `_Series` field). op({ id: "crm.tasks.repeat.get", group: "crm", summary: "How a task repeats (rule, RRULE, trigger, ends, next due).", cli: "dench crm tasks repeat", method: "GET", path: "/crm/tasks/{entryId}/repeat", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/taskRecurrences:getForTask"), }), op({ id: "crm.tasks.repeat.set", group: "crm", summary: "Make a task repeat, or change how it repeats (JSON rule or RRULE). An undated task gets its due date anchored on the first matching day from today.", cli: "dench crm tasks repeat", method: "PUT", path: "/crm/tasks/{entryId}/repeat", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/taskRecurrences:setForTask"), }), op({ id: "crm.tasks.repeat.clear", group: "crm", summary: "Stop a task repeating (past occurrences are kept).", cli: "dench crm tasks repeat --off", method: "DELETE", path: "/crm/tasks/{entryId}/repeat", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/taskRecurrences:clearForTask"), }), op({ id: "crm.tasks.repeat.skip", group: "crm", summary: "Skip the current occurrence: the next one is created now.", cli: "dench crm tasks repeat --skip", method: "POST", path: "/crm/tasks/{entryId}/repeat/skip", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/taskRecurrences:skip"), }), op({ id: "crm.tasks.repeat.pause", group: "crm", summary: "Pause a repeating task.", cli: "dench crm tasks repeat --pause", method: "POST", path: "/crm/tasks/{entryId}/repeat/pause", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/taskRecurrences:pause"), }), op({ id: "crm.tasks.repeat.resume", group: "crm", summary: "Resume a paused repeating task.", cli: "dench crm tasks repeat --resume", method: "POST", path: "/crm/tasks/{entryId}/repeat/resume", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/taskRecurrences:resume"), }), op({ id: "crm.tasks.series.list", group: "crm", summary: "Every repeating-task series in the workspace.", cli: "dench crm tasks series", method: "GET", path: "/crm/tasks/series", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/taskRecurrences:listSeries"), }), op({ id: "crm.tasks.series.occurrences", group: "crm", summary: "Every occurrence (task) of one series.", cli: "dench crm tasks series --occurrences", method: "GET", path: "/crm/tasks/series/{seriesId}/occurrences", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/taskRecurrences:listOccurrences"), }), op({ id: "crm.entries.create", group: "crm", summary: "Create a CRM entry.", cli: "dench crm entries create", method: "POST", path: "/crm/objects/{objectName}/entries", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/entries:create", "bearer", { includeRunId: true, }), }), op({ id: "crm.entries.createMany", group: "crm", summary: "Create many CRM entries.", cli: "dench crm entries create-many", method: "POST", path: "/crm/objects/{objectName}/entries/batch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/entries:createMany", "bearer", { includeRunId: true, }), }), op({ id: "crm.entries.update", group: "crm", summary: "Update a CRM entry.", cli: "dench crm entries update", method: "PATCH", path: "/crm/objects/{objectName}/entries/{entryId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/entries:update", "bearer", { includeRunId: true, }), }), op({ id: "crm.entries.updateMany", group: "crm", summary: "Update many CRM entries.", cli: "dench crm entries update-many", method: "PATCH", path: "/crm/objects/{objectName}/entries/batch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/batch:apply", "bearer", { includeRunId: true, }), }), op({ id: "crm.entries.delete", group: "crm", summary: "Delete a CRM entry.", cli: "dench crm entries delete", method: "DELETE", path: "/crm/objects/{objectName}/entries/{entryId}", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/entries:remove", "bearer", { includeRunId: true, }), // objectName only names the path; the entry id alone identifies it. transformArgs: (input) => ({ entryId: input.entryId }), }, }), op({ id: "crm.entries.bulkDelete", group: "crm", summary: "Bulk delete CRM entries.", cli: "dench crm entries bulk-delete", method: "POST", path: "/crm/objects/{objectName}/entries/delete", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/crm/entries:bulkDelete", "bearer", { includeRunId: true, }), transformArgs: (input) => ({ entryIds: input.entryIds }), }, }), op({ id: "crm.cells.get", group: "crm", summary: "Get a CRM cell value.", cli: "dench crm cells get", method: "GET", path: "/crm/objects/{objectName}/entries/{entryId}/fields/{fieldName}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/cells:get"), }), op({ id: "crm.cells.set", group: "crm", summary: "Set a CRM cell value.", cli: "dench crm cells set", method: "PUT", path: "/crm/objects/{objectName}/entries/{entryId}/fields/{fieldName}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/cells:set", "bearer", { includeRunId: true, }), }), op({ id: "crm.cells.setMany", group: "crm", summary: "Set many CRM cell values.", cli: "dench crm cells set-many", method: "POST", path: "/crm/cells/batch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/cells:setMany", "bearer", { includeRunId: true, }), }), op({ id: "crm.cells.append", group: "crm", summary: "Append to a CRM cell.", cli: "dench crm cells append", method: "POST", path: "/crm/objects/{objectName}/entries/{entryId}/fields/{fieldName}/append", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/cells:append", "bearer", { includeRunId: true, }), }), op({ id: "crm.query", group: "crm", summary: "Query CRM entries.", cli: "dench crm query [--with-references]", method: "POST", path: "/crm/query", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/crm/query:runQueryEntries"), }), op({ id: "crm.queryPage", group: "crm", summary: "Scan CRM entries with a resumable cursor. Empty filtered pages can have more results.", cli: "dench crm query --page", method: "POST", path: "/crm/query/page", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/query:scanEntries"), }), op({ id: "crm.sql", group: "crm", summary: "Run CRM SQL-compatible query.", cli: "dench crm sql", method: "POST", path: "/crm/sql", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/crm/query:runQueryEntries"), }), op({ id: "crm.aggregate", group: "crm", summary: "Aggregate CRM entries.", cli: "dench crm aggregate", method: "POST", path: "/crm/aggregate", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/query:aggregate"), }), op({ id: "crm.search", group: "crm", summary: "Search CRM entries.", cli: "dench crm search [--with-references]", method: "GET", path: "/crm/search", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/query:search"), }), op({ id: "crm.search.page", group: "crm", summary: "Search bounded pages of readable CRM entries, optionally projecting cells. Always follow continueCursor until isDone.", cli: "dench crm search --page", method: "GET", path: "/crm/search/page", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/query:searchPage"), }), op({ id: "crm.statuses.list", group: "crm", summary: "List CRM statuses.", cli: "dench crm statuses list", method: "GET", path: "/crm/objects/{objectName}/statuses", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/statuses:list"), }), op({ id: "crm.statuses.set", group: "crm", summary: "Set CRM statuses.", cli: "dench crm statuses set", method: "PUT", path: "/crm/objects/{objectName}/statuses", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/statuses:upsertMany"), }), op({ id: "crm.batch", group: "crm", summary: "Apply a CRM batch.", cli: "dench crm batch", method: "POST", path: "/crm/batch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/batch:apply", "bearer", { includeRunId: true, }), }), op({ id: "crm.transaction.begin", group: "crm", summary: "Begin a CRM transaction.", cli: "dench crm transaction begin", method: "POST", path: "/crm/transactions", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/transaction:begin"), }), op({ id: "crm.transaction.add", group: "crm", summary: "Add an operation to a CRM transaction.", cli: "dench crm transaction add", method: "POST", path: "/crm/transactions/{txnId}/operations", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/transaction:addOp"), }), op({ id: "crm.transaction.commit", group: "crm", summary: "Commit a CRM transaction.", cli: "dench crm transaction commit", method: "POST", path: "/crm/transactions/{txnId}/commit", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/transaction:commit", "bearer", { includeRunId: true, }), }), op({ id: "crm.transaction.abort", group: "crm", summary: "Abort a CRM transaction.", cli: "dench crm transaction abort", method: "POST", path: "/crm/transactions/{txnId}/abort", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/transaction:abort"), }), op({ id: "crm.transaction.inspect", group: "crm", summary: "Inspect a CRM transaction.", cli: "dench crm transaction inspect", method: "GET", path: "/crm/transactions/{txnId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/transaction:inspect"), }), op({ id: "crm.docs.list", group: "crm", summary: "List CRM documents.", cli: "dench crm docs list", method: "GET", path: "/crm/docs", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/documents:list"), }), op({ id: "crm.docs.create", group: "crm", summary: "Create a CRM document.", cli: "dench crm docs create", method: "POST", path: "/crm/docs", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/documents:create"), }), op({ id: "crm.docs.link", group: "crm", summary: "Link a CRM document.", cli: "dench crm docs link", method: "POST", path: "/crm/docs/link", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/documents:link"), }), op({ id: "crm.docs.delete", group: "crm", summary: "Delete a CRM document page; its workspace file stays and nested pages move up.", cli: "dench crm docs delete", method: "DELETE", path: "/crm/docs/{documentId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/documents:remove"), }), op({ id: "crm.actions.list", group: "crm", summary: "List CRM actions.", cli: "dench crm actions list", method: "GET", path: "/crm/actions", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/actions:list"), }), op({ id: "crm.actions.create", group: "crm", summary: "Create or upsert an action button on a named action column (fieldName is the column header; a column can hold multiple buttons — reuse fieldName to add more).", cli: "dench crm actions create", method: "POST", path: "/crm/actions", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/actions:defineAction"), }), op({ id: "crm.actions.update", group: "crm", summary: "Update an action button on a CRM field.", cli: "dench crm actions edit", method: "PATCH", path: "/crm/actions/{actionId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/actions:defineAction"), }), op({ id: "crm.actions.delete", group: "crm", summary: "Delete an action button from a CRM field.", cli: "dench crm actions delete", method: "DELETE", path: "/crm/actions/{actionId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/actions:deleteAction"), }), op({ id: "crm.actions.run", group: "crm", summary: "Run a CRM action on one or more entries.", cli: "dench crm actions run", method: "POST", path: "/crm/actions/{actionId}/runs", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/crm/actions:run"), }), op({ id: "crm.members.list", group: "crm", summary: "List workspace members for CRM user fields.", cli: "dench members list", method: "GET", path: "/members", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/members:list"), }), // Member profiles — what the profile panel shows when a member is clicked. // Presence comes from the app's heartbeat; bearer callers never count. op({ id: "members.presence", group: "members", summary: "Live presence (online / away / offline) for workspace members.", cli: "dench members presence", method: "GET", path: "/members/presence", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/memberProfiles:getPresence"), transformArgs: (input) => ({ ...(typeof input.userIds === "string" && input.userIds.length > 0 ? { userIds: input.userIds.split(",").map((id) => id.trim()) } : Array.isArray(input.userIds) ? { userIds: input.userIds } : {}), }), }, }), op({ id: "members.get", group: "members", summary: "A member's profile: name, email, tagline, role, timezone, presence, open task count.", cli: "dench members get", method: "GET", path: "/members/{userId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/memberProfiles:getMemberProfile"), }), op({ id: "members.activity", group: "members", summary: "A member's recent comments and record changes across the CRM, newest first.", cli: "dench members activity", method: "GET", path: "/members/{userId}/activity", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/memberProfiles:listMemberActivity"), transformArgs: (input) => ({ userId: input.userId, ...(typeof input.limit === "number" ? { limit: input.limit } : {}), ...(typeof input.kinds === "string" && input.kinds.length > 0 ? { kinds: input.kinds.split(",").map((kind) => kind.trim()) } : Array.isArray(input.kinds) ? { kinds: input.kinds } : {}), }), }, }), op({ id: "members.tasks", group: "members", summary: "Open tasks assigned to a member (plus ones completed today).", cli: "dench members tasks", method: "GET", path: "/members/{userId}/tasks", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/tasks:listOpenTasksForMember"), }), op({ id: "me.profile.get", group: "members", summary: "Your own member profile.", cli: "dench members me", method: "GET", path: "/me/profile", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/memberProfiles:getMyProfile"), }), op({ id: "me.profile.update", group: "members", summary: "Update your name, tagline, avatar URL, or timezone.", cli: "dench members me --name … --tagline … --timezone …", method: "PATCH", path: "/me/profile", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/memberProfiles:updateMyProfile"), }), op({ id: "me.avatar.uploadUrl", group: "members", summary: "Get a one-time URL to POST an avatar image to; then PUT /me/avatar with the returned storageId.", cli: "dench members me --avatar ", method: "POST", path: "/me/avatar/upload-url", requestSchema: noBody, responseSchema: anyResponse, backend: convex( "mutation", "functions/memberProfiles:generateAvatarUploadUrl", ), }), op({ id: "me.avatar.set", group: "members", summary: "Set your avatar from an uploaded storageId.", cli: "dench members me --avatar ", method: "PUT", path: "/me/avatar", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/memberProfiles:setMyAvatar"), }), op({ id: "me.avatar.clear", group: "members", summary: "Remove your avatar.", cli: "dench members me --clear-avatar", method: "DELETE", path: "/me/avatar", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/memberProfiles:clearMyAvatar"), }), op({ id: "crm.reports.generate", group: "crm", summary: "Generate a CRM report.", cli: "dench crm reports generate", method: "POST", path: "/crm/reports", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/reports:generate"), }), // CRM analytics — the dashboard strip above every object table. One // contract for the UI, the CLI and this API: `computePanels` is the only // chart math; a dashboard belongs to the object or to ONE saved view. op({ id: "crm.analytics.get", group: "crm", summary: "Read an object's analytics dashboard: panels, field roles, the strip's collapsed flag, the named view's own dashboard (if any), and the fields panels may bucket on (synthetic $createdAt / $location last).", cli: "dench crm analytics get", method: "GET", path: "/crm/objects/{objectName}/analytics", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/analytics:getAnalyticsConfig"), }), op({ id: "crm.analytics.compute", group: "crm", summary: "Compute chart-ready results (buckets / series / values) for the stored dashboard, a subset of its panels, or ad-hoc panel definitions — optionally scoped to a saved view, a period preset (with the previous interval as previousValue) and extra filters. Same query the strip renders from.", cli: "dench crm analytics compute", method: "POST", path: "/crm/objects/{objectName}/analytics/compute", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/crm/analytics:computePanels"), }), op({ id: "crm.analytics.save", group: "crm", summary: "Create or update one panel (upsert by id). With viewName the write lands on that saved view's own dashboard, forking a copy if it was still inheriting the object's.", cli: "dench crm analytics save", method: "PUT", path: "/crm/objects/{objectName}/analytics/panels/{panelId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:saveAnalyticsPanel"), }), op({ id: "crm.analytics.update", group: "crm", summary: "Edit part of one stored panel: only the keys in `patch` change (title, type, metric, metricField, groupBy, dateField, dateBucket, filters, derived, span, hidden, sortOrder); null clears an optional key. What the card's ⋮ menu writes.", cli: "dench crm analytics update", method: "PATCH", path: "/crm/objects/{objectName}/analytics/panels/{panelId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:updateAnalyticsPanel"), }), op({ id: "crm.analytics.hide", group: "crm", summary: "Take a chart off the strip above the table (hidden: true). It stays in the full analytics view, counted by the strip's “+N more”.", cli: "dench crm analytics hide", method: "POST", path: "/crm/objects/{objectName}/analytics/panels/{panelId}/hide", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:updateAnalyticsPanel"), }), op({ id: "crm.analytics.show", group: "crm", summary: "Bring a hidden chart back onto the strip (hidden: false).", cli: "dench crm analytics show", method: "POST", path: "/crm/objects/{objectName}/analytics/panels/{panelId}/show", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:updateAnalyticsPanel"), }), op({ id: "crm.analytics.resize", group: "crm", summary: "Set a card's width on the 6-track dashboard grid: span 2 (third), 3 (half), 4 (two thirds), 6 (full), or null for the chart kind's default.", cli: "dench crm analytics resize", method: "POST", path: "/crm/objects/{objectName}/analytics/panels/{panelId}/resize", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:updateAnalyticsPanel"), }), op({ id: "crm.analytics.delete", group: "crm", summary: "Delete one panel (idempotent). Template panels are tombstoned so a re-install never resurrects them.", cli: "dench crm analytics delete", method: "DELETE", path: "/crm/objects/{objectName}/analytics/panels/{panelId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:deleteAnalyticsPanel"), }), op({ id: "crm.analytics.reorder", group: "crm", summary: "Persist the full panel order. panelIds must name every stored panel of the targeted dashboard.", cli: "dench crm analytics reorder", method: "POST", path: "/crm/objects/{objectName}/analytics/reorder", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/crm/analytics:reorderAnalyticsPanels", ), }), op({ id: "crm.analytics.roles", group: "crm", summary: "Set the object's analytics setup — the field roles shared by every chart and view (amount, stage, won/lost/converted values, stageProbabilities, closeDate, recurrence, owner, location). Replaces the mapping; with merge: true only the keys given change and null clears one.", cli: "dench crm analytics roles", method: "PUT", path: "/crm/objects/{objectName}/analytics/roles", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/crm/analytics:setAnalyticsFieldRoles", ), }), op({ id: "crm.analytics.collapse", group: "crm", summary: "Collapse the analytics strip above the object's table (team-shared, like saved views).", cli: "dench crm analytics collapse", method: "POST", path: "/crm/objects/{objectName}/analytics/collapse", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/crm/analytics:setAnalyticsStripCollapsed", ), }), op({ id: "crm.analytics.expand", group: "crm", summary: "Expand the analytics strip above the object's table.", cli: "dench crm analytics expand", method: "POST", path: "/crm/objects/{objectName}/analytics/expand", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/crm/analytics:setAnalyticsStripCollapsed", ), }), op({ id: "crm.analytics.scope", group: "crm", summary: 'Give a saved view its own dashboard (mode "own": starts as a copy of the object\'s panels) or hand it back to the object\'s shared one (mode "shared").', cli: "dench crm analytics scope", method: "POST", path: "/crm/objects/{objectName}/views/{viewName}/analytics/scope", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/analytics:setViewAnalyticsMode"), }), op({ id: "crm.enrich.cell", group: "crm", summary: "Request enrichment for one CRM cell.", cli: "dench crm enrich cell", method: "POST", path: "/crm/enrich/cell", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/enrich:requestCellEnrichment"), }), op({ id: "crm.enrich.object", group: "crm", summary: "Request enrichment for a CRM object.", cli: "dench crm enrich object", method: "POST", path: "/crm/enrich/object", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/enrich:requestObjectEnrichment"), }), op({ id: "crm.people.search", group: "crm", summary: "Search for people through the enrichment gateway. `companyDomains` takes up to 1000 domains (e.g. a resolved company list; `dench leads find --company-list ` resolves one for you) — the gateway fans large lists out as sequential vendor-safe chunks behind one `nextPageToken` stream.", cli: "dench crm people search", method: "POST", path: "/enrichment/people/search", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/people/search"), }), // One route, two CLI spellings: `dench companies find` is the explorer's // name for the same gateway search. It used to be a second op on the same // method+path, which the API dispatcher cannot distinguish (and the // registry test rejects) — so it is an alias, like `dench leads find` is // for people. op({ id: "crm.companies.search", group: "crm", summary: "Search for companies through the enrichment gateway (also `dench companies find`).", cli: "dench crm companies search", aliases: ["dench companies find"], method: "POST", path: "/enrichment/company/search", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/company/search"), }), op({ id: "enrichment.jobs.search", group: "crm", summary: "Search job postings through Dench find. `companyDomains` takes up to 1000 domains (chunked automatically); `dench jobs find --company-list ` resolves a saved company list into it.", cli: "dench jobs find", method: "POST", path: "/enrichment/jobs/search", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/jobs/search"), }), op({ id: "enrichment.person", group: "crm", summary: "Look up one person profile.", cli: "dench crm enrich cell", method: "POST", path: "/enrichment/person", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/person"), }), op({ id: "enrichment.company", group: "crm", summary: "Look up one company profile.", cli: "dench crm enrich cell", method: "POST", path: "/enrichment/company", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/company"), }), op({ id: "enrichment.person.contact", group: "crm", summary: "Look up verified emails and phones for people.", cli: "dench crm enrich cell", method: "POST", path: "/enrichment/person/contact", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/person/contact"), }), op({ id: "enrichment.profiles.bulk", group: "crm", summary: "Look up up to 100 people or company profiles without buying emails or phones. Pass kind (people|companies) and lookups; results preserve inputIndex. People require linkedinUrl or email; companies require domain or linkedinUrl.", cli: "dench crm enrich profiles", method: "POST", path: "/enrichment/profiles/bulk", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/profiles/bulk"), }), op({ id: "enrichment.company.related", group: "crm", summary: "Read company employees, founders, funding, investors, acquisitions, or investments. Pass domain or linkedinUrl, kind, limit (1–100), and optional pageToken. Does not import CRM records or buy contacts.", cli: "dench companies related", method: "POST", path: "/enrichment/company/related", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/company/related"), }), op({ id: "enrichment.search.preview", group: "crm", summary: "Preview how many records a find would return.", cli: "dench leads find --limit 1", method: "POST", path: "/enrichment/search/preview", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/enrichment/search/preview"), }), op({ id: "enrichment.filters.options", group: "crm", summary: "List Lead Finder filter vocabularies: exact enum values (seniorities, functions, industries, headcount, company types, languages) and curated suggestions (titles, locations, company HQ, universities, skills, keywords).", cli: "dench leads filters", method: "GET", path: "/local/enrichment/filter-options", requestSchema: noBody, responseSchema: anyResponse, backend: local( "Catalogs are bundled with the CLI. Over HTTPS, fetch https://dench.com/references/find-filter-options.json instead.", ), }), op({ id: "crm.templates.list", group: "crm", summary: "List one-click CRM templates (objects + automations + sample rows) with per-workspace installed flags.", cli: "dench crm templates list", method: "GET", path: "/crm/templates", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/templates:listAvailable"), }), op({ id: "crm.templates.install", group: "crm", summary: "Install a CRM template. Idempotent: re-installs top up missing pieces and never duplicate objects, automations, or sample rows.", cli: "dench crm templates install", method: "POST", path: "/crm/templates/{templateId}/install", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/templates:install"), }), op({ id: "crm.templates.removeSampleData", group: "crm", summary: 'Delete a CRM template\'s sample rows (tagged "Sample"); schema, views, and automations stay.', cli: "dench crm templates remove-sample-data", method: "POST", path: "/crm/templates/{templateId}/remove-sample-data", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/templates:removeSampleData"), }), op({ id: "crm.automations.list", group: "crm", summary: "List deterministic CRM automations (rules that fire inside entry writes, e.g. qualified prospect → opportunity).", cli: "dench crm automations list", method: "GET", path: "/crm/automations", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/crm/automations:list"), }), op({ id: "crm.automations.setEnabled", group: "crm", summary: "Enable or disable a CRM automation.", cli: "dench crm automations enable|disable", method: "PATCH", path: "/crm/automations/{automationId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/crm/automations:setEnabled"), }), op({ id: "crm.import", group: "local", summary: "Import a local CSV into a CRM object.", cli: "dench crm import", method: "POST", path: "/local/crm/import", requestSchema: anyObject, responseSchema: anyResponse, backend: local( "Reads a CSV from the caller's local filesystem. Over the API, parse the CSV client-side and POST rows to /crm/objects/{objectName}/entries/batch.", ), }), op({ id: "crm.export", group: "local", summary: "Export a CRM object to a local CSV.", cli: "dench crm export", method: "POST", path: "/local/crm/export", requestSchema: anyObject, responseSchema: anyResponse, backend: local( "Writes a CSV to the caller's local filesystem. Over the API, call POST /crm/query and format CSV client-side.", ), }), // Meetings. A meeting is an ordinary CRM record (`/crm/objects/meeting/…` // creates, updates and deletes it); these endpoints reach the side tables // the CRM verbs cannot — transcript, action items, attendance — so a call // imported from another notetaker gets the same tabs a recorded one has. op({ id: "meetings.list", group: "meetings", summary: "List meetings, newest first, with status and participants.", cli: "dench meetings list", method: "GET", path: "/meetings", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex("query", "functions/meetings/library:listMeetings"), transformArgs: (input) => ({ paginationOpts: paginationOptsFrom(input, 50), }), }, }), op({ id: "meetings.transcript.list", group: "meetings", summary: "Read a meeting's transcript, in order, one page at a time.", cli: "dench meetings transcript get", method: "GET", path: "/meetings/{meetingEntryId}/transcript", requestSchema: noBody, responseSchema: anyResponse, backend: { ...convex( "query", "functions/meetings/transcripts:listMeetingTranscript", ), transformArgs: (input) => ({ meetingEntryId: input.meetingEntryId, paginationOpts: paginationOptsFrom(input, 100), }), }, }), op({ id: "meetings.transcript.append", group: "meetings", summary: "Append up to 500 transcript segments to a meeting (speakers with an email link to existing people).", cli: "dench meetings transcript set", method: "POST", path: "/meetings/{meetingEntryId}/transcript", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/meetings/importer:appendTranscriptSegments", ), }), op({ id: "meetings.transcript.clear", group: "meetings", summary: "Delete a batch of a meeting's transcript segments; repeat until `done` before re-appending.", cli: "dench meetings transcript clear", method: "DELETE", path: "/meetings/{meetingEntryId}/transcript", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/meetings/importer:clearTranscript"), }), op({ id: "meetings.actionItems.list", group: "meetings", summary: "List a meeting's action items.", cli: "dench meetings action-items list", method: "GET", path: "/meetings/{meetingEntryId}/action-items", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/meetings/actionItems:listActionItems"), }), op({ id: "meetings.actionItems.add", group: "meetings", summary: "Add action items to a meeting; `replace: true` swaps the whole list.", cli: "dench meetings action-items set", method: "POST", path: "/meetings/{meetingEntryId}/action-items", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/meetings/importer:addActionItems"), }), op({ id: "meetings.actionItems.setDone", group: "meetings", summary: "Tick or untick one action item.", cli: "dench meetings action-items done", method: "PATCH", path: "/meetings/action-items/{itemId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/meetings/actionItems:setActionItemDone", ), }), op({ id: "meetings.actionItems.delete", group: "meetings", summary: "Delete one action item.", cli: "dench meetings action-items delete", method: "DELETE", path: "/meetings/action-items/{itemId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex( "mutation", "functions/meetings/actionItems:deleteActionItem", ), }), op({ id: "meetings.participants.list", group: "meetings", summary: "Who was in the meeting, merged with the linked calendar invite.", cli: "dench meetings participants list", method: "GET", path: "/meetings/{meetingEntryId}/participants", requestSchema: noBody, responseSchema: anyResponse, backend: convex( "query", "functions/meetings/participantLinks:listMeetingParticipantRoster", ), }), op({ id: "meetings.participants.set", group: "meetings", summary: "Record attendees by name/email; known members and existing people are linked, nobody is created.", cli: "dench meetings participants set", method: "PUT", path: "/meetings/{meetingEntryId}/participants", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/meetings/importer:setParticipants"), }), op({ id: "meetings.sharing.get", group: "meetings", summary: "Whether a meeting is shared with the whole workspace, and whether you may change that.", cli: "dench meetings share", method: "GET", path: "/meetings/{meetingEntryId}/sharing", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/meetings/sharing:getMeetingSharing"), }), op({ id: "meetings.sharing.set", group: "meetings", summary: "Share one meeting with the workspace, or take it back to owner + invitees.", cli: "dench meetings share --workspace", method: "PUT", path: "/meetings/{meetingEntryId}/sharing", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/meetings/sharing:setMeetingSharing"), }), op({ id: "meetings.import", group: "local", summary: "Import calls from a local CSV/JSONL: meeting fields plus transcript, action items and attendees per row.", cli: "dench meetings import", method: "POST", path: "/local/meetings/import", requestSchema: anyObject, responseSchema: anyResponse, backend: local( "Reads a CSV/JSONL from the caller's local filesystem. Over the API: POST /crm/objects/meeting/entries/batch with seedKey per row, then POST /meetings/{meetingEntryId}/transcript, POST …/action-items and PUT …/participants for each created meeting.", ), }), // Managed email campaigns. op({ id: "email.identity.verify", group: "email", summary: "Start Dench Emailing Service sender verification.", cli: "dench email identity verify", method: "POST", path: "/email/identities/verify", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/email/identities/verify"), }), op({ id: "email.identity.create", group: "email", summary: "Create a durable Dench email sending identity record.", cli: "dench email identity verify", method: "POST", path: "/email/sending-identities", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:createIdentity"), }), op({ id: "email.identity.refresh", group: "email", summary: "Refresh a Dench Emailing Service sender verification status.", cli: "dench email identity status --identity-id", method: "POST", path: "/email/sending-identities/{identityId}/status", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/emailCampaignsNode:refreshIdentityStatus", ), }), op({ id: "email.identity.beginVerification", group: "email", summary: "Begin the full Dench sender verification flow for a sending identity.", cli: "dench email identity verify", method: "POST", path: "/email/sending-identities/{identityId}/verify", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/emailCampaignsNode:beginSenderVerification", ), }), op({ id: "email.identity.status", group: "email", summary: "Read Dench Emailing Service sender verification status.", cli: "dench email identity status", method: "GET", path: "/email/identities/{identity}/status", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("GET", "/email/identities/{identity}/status"), }), op({ id: "email.identity.list", group: "email", summary: "List the workspace's email sending identities.", cli: "dench email identity list", method: "GET", path: "/email/sending-identities", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailCampaigns:listIdentities"), }), op({ id: "email.identity.dns", group: "email", summary: "Fetch DNS records (DKIM/SPF/DMARC) for a pending domain sender verification.", cli: "dench email identity dns", method: "GET", path: "/email/sending-identities/{identityId}/dns", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/emailCampaignsNode:refreshIdentityStatus", ), }), op({ id: "email.identity.disable", group: "email", summary: "Soft-disable a sender (or every sender on a domain) for the workspace; sends are blocked until restored.", cli: "dench email identity disable", method: "POST", path: "/email/sending-identities/{identity}/disable", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:disableIdentity"), }), op({ id: "email.identity.restore", group: "email", summary: "Restore a previously disabled sender for the workspace.", cli: "dench email identity restore", method: "POST", path: "/email/sending-identities/{identity}/restore", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:restoreIdentity"), }), op({ id: "email.identity.delete", group: "email", summary: "Permanently delete a sender (or every sender on a domain) from the workspace; re-adding it requires verification from scratch.", cli: "dench email identity delete", method: "DELETE", path: "/email/sending-identities/{identity}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:deleteIdentity"), }), op({ id: "email.inbox.search", group: "email", summary: "Search authorized synced mailbox headers with bounded cursor pagination. isDone covers stored records, not provider completeness.", cli: "dench email inbox search", method: "GET", path: "/email/inbox/search", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailInbox:search"), }), op({ id: "email.inbox.thread", group: "email", summary: "Read a bounded page of synced headers using an exact thread handle from inbox search; bodies remain on demand.", cli: "dench email inbox thread", method: "GET", path: "/email/inbox/threads/{threadHandle}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailInbox:thread"), }), op({ id: "email.inbox.message", group: "email", summary: "Read a bounded stored body using an exact message handle from inbox search; rechecks mailbox and record privacy.", cli: "dench email inbox message", method: "GET", path: "/email/inbox/messages/{messageHandle}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailInbox:message"), }), op({ id: "email.drafts.create", group: "email", summary: "Create an editable Dench email draft and synchronize its selected native mailbox asynchronously. Use a stable requestId for retries.", cli: "dench email draft create", method: "POST", path: "/email/drafts", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailDrafts:create"), }), op({ id: "email.drafts.get", group: "email", summary: "Read an email draft, eligible senders and provider sync status.", cli: "dench email draft show", method: "GET", path: "/email/drafts/{draftId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailDrafts:get"), }), op({ id: "email.drafts.senders", group: "email", summary: "List native mailboxes this caller may use.", cli: "dench email draft senders", method: "GET", path: "/email/drafts/senders", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailDrafts:listSenders"), }), op({ id: "email.drafts.update", group: "email", summary: "Save draft content using expectedRevision; sender changes require a new draft after provider binding.", cli: "dench email draft update", method: "PATCH", path: "/email/drafts/{draftId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailDrafts:update"), }), op({ id: "email.drafts.send", group: "email", summary: "Send the current Dench draft revision through its native mailbox. Repeated requests share a durable receipt.", cli: "dench email draft send", method: "POST", path: "/email/drafts/{draftId}/send", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailDrafts:send"), }), op({ id: "email.drafts.upload", group: "email", summary: "Upload attachment bytes authorized against this draft before adding its storage reference.", cli: "dench email draft attach", method: "POST", path: "/email/drafts/{draftId}/attachments", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/emailDraftsNode:uploadAttachment"), }), op({ id: "email.drafts.signature", group: "email", summary: "Load the selected mailbox signature for explicit visible insertion.", cli: "dench email draft signature", method: "GET", path: "/email/drafts/{draftId}/signature", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/emailDraftsNode:loadSignature"), }), op({ id: "email.native.send", group: "email", summary: "Send through a native Gmail or Outlook mailbox with a durable request receipt. Only use for an explicit send request.", cli: "dench email send [--mailbox ]", method: "POST", path: "/email/native/send", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/emailDrafts:create"), fixedArgs: { mode: "send" }, }, }), op({ id: "email.send", group: "email", summary: "Send one raw email (no campaign): to/cc/bcc, custom from-name and reply-to, optional reply threading via replyToMessageId.", cli: "dench email send --from ", method: "POST", path: "/email/send", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/emailCampaignsNode:sendEmail"), }), op({ id: "email.message.get", group: "email", summary: "Read a raw email message's status and open/click counters.", cli: "dench email message status", method: "GET", path: "/email/messages/{messageId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailCampaigns:getMessage"), }), op({ id: "email.message.list", group: "email", summary: "List recent raw email messages for the workspace.", cli: "dench email message list", method: "GET", path: "/email/messages", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailCampaigns:listMessages"), }), op({ id: "email.template.create", group: "email", summary: "Create a managed email campaign template.", cli: "dench email template create", method: "POST", path: "/email/templates", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:createTemplate"), }), op({ id: "email.template.preview", group: "email", summary: "Preview a managed email campaign template.", cli: "dench email template preview", method: "POST", path: "/email/templates/{templateId}/preview", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailCampaigns:previewTemplate"), }), op({ id: "email.campaign.create", group: "email", summary: "Create a managed email campaign draft from a connected inbox or verified sender.", cli: "dench email campaign create", method: "POST", path: "/email/campaigns", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:createCampaign"), }), op({ id: "email.campaign.get", group: "email", summary: "Read campaign status and counters.", cli: "dench email campaign status", method: "GET", path: "/email/campaigns/{campaignId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailCampaigns:getCampaign"), }), op({ id: "email.campaign.recipients.add", group: "email", summary: "Add recipients to a managed email campaign.", cli: "dench email campaign recipients add", method: "POST", path: "/email/campaigns/{campaignId}/recipients", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:addRecipients"), }), op({ id: "email.campaign.preview", group: "email", summary: "Preview rendered messages for a campaign sample.", cli: "dench email campaign preview", method: "POST", path: "/email/campaigns/{campaignId}/preview", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailCampaigns:previewCampaign"), }), op({ id: "email.campaign.submit", group: "email", summary: "Submit a campaign for approval or sending.", cli: "dench email campaign submit", method: "POST", path: "/email/campaigns/{campaignId}/submit", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:submitCampaign"), }), op({ id: "email.campaign.pause", group: "email", summary: "Pause a sending email campaign.", cli: "dench email campaign pause", method: "POST", path: "/email/campaigns/{campaignId}/pause", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:pauseCampaign"), }), op({ id: "email.campaign.resume", group: "email", summary: "Resume a paused email campaign.", cli: "dench email campaign resume", method: "POST", path: "/email/campaigns/{campaignId}/resume", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:resumeCampaign"), }), op({ id: "email.campaign.cancel", group: "email", summary: "Cancel an email campaign.", cli: "dench email campaign cancel", method: "POST", path: "/email/campaigns/{campaignId}/cancel", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailCampaigns:cancelCampaign"), }), op({ id: "email.campaign.list", group: "email", summary: "List managed email campaigns (paginated, optional status filter).", cli: "dench email campaign list", method: "GET", path: "/email/campaigns", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("query", "functions/emailCampaigns:listCampaigns"), transformArgs: (input) => ({ status: input.status, paginationOpts: paginationOptsFrom(input, 25), }), }, }), op({ id: "email.campaign.recipients.list", group: "email", summary: "List a campaign's recipients (paginated, optional status filter).", cli: "dench email campaign recipients list", method: "GET", path: "/email/campaigns/{campaignId}/recipients", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("query", "functions/emailCampaigns:listRecipients"), transformArgs: (input) => ({ campaignId: input.campaignId, status: input.status, includePeople: input.includePeople, paginationOpts: paginationOptsFrom(input, 50), }), }, }), op({ id: "email.campaign.recipients.addFromObject", group: "email", summary: "Add campaign recipients from a CRM object: pick the object, the email field, and the entry ids.", cli: "dench email campaign recipients add-from-object", method: "POST", path: "/email/campaigns/{campaignId}/recipients/from-object", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/emailCampaigns:addRecipientsFromCrm", ), }), // Managed email sequences (multi-step drip cadences). op({ id: "email.sequence.create", group: "email", summary: "Create a follow-up sequence from a connected inbox or verified sender.", cli: "dench email sequence create", method: "POST", path: "/email/sequences", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:createSequence"), }), op({ id: "email.sequence.update", group: "email", summary: "Update a sequence's name, sender, or reply/stop settings.", cli: "dench email sequence update", method: "POST", path: "/email/sequences/{sequenceId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:updateSequence"), }), op({ id: "email.sequence.activate", group: "email", summary: "Activate a sequence so its enrollments begin sending.", cli: "dench email sequence activate", method: "POST", path: "/email/sequences/{sequenceId}/activate", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/emailSequences:setSequenceActive"), fixedArgs: { active: true }, }, }), op({ id: "email.sequence.deactivate", group: "email", summary: "Deactivate a sequence (pause sending, revert to draft).", cli: "dench email sequence deactivate", method: "POST", path: "/email/sequences/{sequenceId}/deactivate", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/emailSequences:setSequenceActive"), fixedArgs: { active: false }, }, }), op({ id: "email.sequence.archive", group: "email", summary: "Archive a sequence (stops all sending).", cli: "dench email sequence archive", method: "POST", path: "/email/sequences/{sequenceId}/archive", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:archiveSequence"), }), op({ id: "email.sequence.list", group: "email", summary: "List the workspace's follow-up sequences.", cli: "dench email sequence list", method: "GET", path: "/email/sequences", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailSequences:listSequences"), }), op({ id: "email.sequence.get", group: "email", summary: "Read a sequence with its ordered steps.", cli: "dench email sequence status", method: "GET", path: "/email/sequences/{sequenceId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailSequences:getSequence"), }), op({ id: "email.sequence.step.add", group: "email", summary: "Add a step (subject, body, and wait delay) to a sequence.", cli: "dench email sequence steps add", method: "POST", path: "/email/sequences/{sequenceId}/steps", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:addStep"), }), op({ id: "email.sequence.step.update", group: "email", summary: "Update a sequence step's subject, body, or delay.", cli: "dench email sequence steps update", method: "POST", path: "/email/sequences/steps/{stepId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:updateStep"), }), op({ id: "email.sequence.step.remove", group: "email", summary: "Remove a step from a sequence.", cli: "dench email sequence steps remove", method: "POST", path: "/email/sequences/steps/{stepId}/remove", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:removeStep"), }), op({ id: "email.sequence.step.reorder", group: "email", summary: "Reorder a sequence's steps.", cli: "dench email sequence steps reorder", method: "POST", path: "/email/sequences/{sequenceId}/steps/reorder", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:reorderSteps"), }), op({ id: "email.sequence.enroll", group: "email", summary: "Enroll explicit recipients into a sequence.", cli: "dench email sequence enroll", method: "POST", path: "/email/sequences/{sequenceId}/enroll", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:enroll"), }), op({ id: "email.sequence.enrollFromObject", group: "email", summary: "Enroll people from a CRM object: pick the object, the email field, and the entry ids.", cli: "dench email sequence enroll-from-object", method: "POST", path: "/email/sequences/{sequenceId}/enroll/from-object", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:enrollFromCrm"), }), op({ id: "email.sequence.enrollments.list", group: "email", summary: "List a sequence's enrollments (paginated, optional status filter).", cli: "dench email sequence enrollments list", method: "GET", path: "/email/sequences/{sequenceId}/enrollments", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("query", "functions/emailSequences:listEnrollments"), transformArgs: (input) => ({ sequenceId: input.sequenceId, status: input.status, paginationOpts: paginationOptsFrom(input, 50), }), }, }), op({ id: "email.sequence.enrollment.pause", group: "email", summary: "Pause one enrollment.", cli: "dench email sequence enrollment pause", method: "POST", path: "/email/sequences/enrollments/{enrollmentId}/pause", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:pauseEnrollment"), }), op({ id: "email.sequence.enrollment.resume", group: "email", summary: "Resume one paused enrollment.", cli: "dench email sequence enrollment resume", method: "POST", path: "/email/sequences/enrollments/{enrollmentId}/resume", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:resumeEnrollment"), }), op({ id: "email.sequence.enrollment.stop", group: "email", summary: "Stop one enrollment (no further steps send).", cli: "dench email sequence enrollment stop", method: "POST", path: "/email/sequences/enrollments/{enrollmentId}/stop", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/emailSequences:stopEnrollment"), }), // Outreach campaigns (email + LinkedIn steps on one schedule): `dench campaign`. op({ id: "campaigns.accounts.linkedin", group: "campaigns", summary: "Discover LinkedIn sender ids, labels, availability and remaining capacity visible to the acting workspace member. Never returns provider credentials.", cli: "dench campaign accounts", method: "GET", path: "/campaigns/accounts/linkedin", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/linkedinAccounts:listSenderAccounts"), }), op({ id: "campaigns.accounts.email", group: "campaigns", summary: "Discover the mailboxes the acting workspace member can send campaign email from: ids for emailMailboxAccountIds, status, warm-up state and when it is done, the daily cold-email cap and what is left today. Never returns provider credentials.", cli: "dench campaign accounts", method: "GET", path: "/campaigns/accounts/email", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/emailMailboxAccounts:listAccounts"), }), op({ id: "campaigns.list", group: "campaigns", summary: "List outreach campaigns (archived ones on request).", cli: "dench campaign list [--with-references]", method: "GET", path: "/campaigns", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:listSequences"), }), op({ id: "campaigns.create", group: "campaigns", summary: "Create a draft outreach campaign with email and LinkedIn sender pools and a business-hours send window in the given timezone.", cli: "dench campaign create", method: "POST", path: "/campaigns", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:createSequence"), }), op({ id: "campaigns.duplicate", group: "campaigns", summary: 'Copy a campaign into a new draft named " copy": the same steps, schedule, sender pools and personalized lines, its lead source paused, and no people. To reach more than 300 people, duplicate and add the next page of the search to the copy.', cli: "dench campaign duplicate", method: "POST", path: "/campaigns/{sequenceId}/duplicate", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:duplicateSequence", ), }), op({ id: "campaigns.get", group: "campaigns", summary: "Read a campaign: settings, steps, people, their current step and hold, last touches, linked leads.", cli: "dench campaign get", method: "GET", path: "/campaigns/{sequenceId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:getSequence"), }), op({ id: "campaigns.analytics", group: "campaigns", summary: "Campaign funnel: enrolled, contacted, invited, accepted, messaged, replied, opens and clicks, and for a workflow how many people took each If / else and Wait for them to accept path (branchStats).", cli: "dench campaign get", method: "GET", path: "/campaigns/{sequenceId}/analytics", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "action", "functions/outreachSequences:computeSequenceAnalytics", ), }), op({ id: "campaigns.live", group: "campaigns", summary: "What the campaign is doing now, what comes next and when, and why it is holding.", cli: "dench campaign get", method: "GET", path: "/campaigns/{sequenceId}/live", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:getSequenceLive"), }), op({ id: "campaigns.pipeline", group: "campaigns", summary: "Where a campaign's people sit in the workspace's lead pipeline right now: each lead Status in order with how many enrolled leads are at it, and how many have no lead or a status outside the pipeline.", cli: "dench campaign pipeline", method: "GET", path: "/campaigns/{sequenceId}/pipeline", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "query", "functions/outreachSequences:getSequenceLeadStatusCounts", ), }), op({ id: "campaigns.update", group: "campaigns", summary: "Change a campaign's name, sender pools, send windows, daily email cap, stop-on-reply, start or end. Works until archived; existing conversations keep their assigned sender.", cli: "dench campaign set", method: "PATCH", path: "/campaigns/{sequenceId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:updateDraft"), }), op({ id: "campaigns.steps.replace", group: "campaigns", summary: "Replace every step of a draft campaign (email, wait, LinkedIn visit, connection request, message, post reaction, InMail, If / else, Wait for them to accept). A workflow (stepModel workflow, or steps with key, next, yesNext or noNext) branches: condition and wait_for_connection steps continue on yesNext or noNext, other steps on next; each points at another step's key or end. Steps are checked first: a field the step type would drop, missing text, {TOKEN} single braces or paths that don't hold together are rejected, not saved. Configure sender pools on the campaign; step sender ids are legacy-only.", cli: "dench campaign steps set", method: "PUT", path: "/campaigns/{sequenceId}/steps", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:replaceSteps"), }), op({ id: "campaigns.steps.update", group: "campaigns", summary: "Edit the text, waits and conditions of a launched (or paused) campaign's steps in place. Each step keeps its id, type and place; pass the stepId and the updatedAt you read, and only the fields you change. A workflow keeps its paths and what each If / else checks (key, next, yesNext, noNext, check, conditionField, conditionOperator); conditionValue and acceptance waits stay editable. Messages already sent or being prepared keep their text; drafts use campaigns.steps.replace.", cli: "dench campaign steps edit", method: "PATCH", path: "/campaigns/{sequenceId}/steps", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:updateSteps"), }), op({ id: "campaigns.personalization.get", group: "campaigns", summary: "Read a campaign's personalized lines (written for each person): their {{keys}} for steps, instructions and fallbacks, how many people have them, and the cost per person.", cli: "dench campaign personalize get", method: "GET", path: "/campaigns/{sequenceId}/personalization", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachPersonalization:getStatus"), }), op({ id: "campaigns.personalization.set", group: "campaigns", summary: "Define up to 5 personalized lines (each used as {{key}} in steps) and what you sell. Saving starts writing missing lines for up to 100 people, about 3 cents a person; for more it returns needsConfirm and writes nothing until campaigns.personalization.write is confirmed.", cli: "dench campaign personalize set", method: "PUT", path: "/campaigns/{sequenceId}/personalization", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachPersonalization:setTemplate", ), }), op({ id: "campaigns.personalization.clear", group: "campaigns", summary: "Turn off a campaign's personalized lines and cancel the ones not yet written. Refused while a step still uses one of the {{keys}}.", cli: "dench campaign personalize clear", method: "DELETE", path: "/campaigns/{sequenceId}/personalization", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachPersonalization:setTemplate", ), }), op({ id: "campaigns.personalization.write", group: "campaigns", summary: "Write personalized lines for the people missing them (mode missing) or rewrite everyone's (mode all). Rewriting everyone or more than 100 people returns needsConfirm and the estimate until called with confirmed: true.", cli: "dench campaign personalize write", method: "POST", path: "/campaigns/{sequenceId}/personalization/write", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachPersonalization:backfill"), }), op({ id: "campaigns.personalization.edit", group: "campaigns", summary: "Set one person's personalized line by hand; an empty value undoes the edit. Braces are removed and text past the line's length is cut.", cli: "dench campaign personalize edit", method: "PUT", path: "/campaigns/people/{enrollmentId}/personalization", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachPersonalization:updateAiVariable", ), }), op({ id: "campaigns.personalization.rewrite", group: "campaigns", summary: "Write a fresh personalized line for one person: one key, or all of their lines when key is left out. About 3 cents.", cli: "dench campaign personalize rewrite", method: "POST", path: "/campaigns/people/{enrollmentId}/personalization/rewrite", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachPersonalization:rewriteForEnrollment", ), }), op({ id: "campaigns.people.addFromCrm", group: "campaigns", summary: "Add CRM records (up to 100 ids per call) to a campaign; returns how many were added and why others were skipped.", cli: "dench campaign add", method: "POST", path: "/campaigns/{sequenceId}/people", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:enrollFromCrm"), }), op({ id: "campaigns.people.add", group: "campaigns", summary: "Add one person by email and/or LinkedIn profile URL.", cli: "dench campaign add --name", method: "POST", path: "/campaigns/{sequenceId}/person", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:addEnrollment"), }), op({ id: "campaigns.preview", group: "campaigns", summary: "Show the message one person gets at one email or LinkedIn step, with their fields and personalized lines filled in: subject, body, sender, recipient, merge fields still missing, warnings, and why the step would be skipped. Nothing is sent, prepared or charged.", cli: "dench campaign preview", method: "GET", path: "/campaigns/{sequenceId}/preview", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachMessagePreview:preview"), }), op({ id: "campaigns.launch.preview", group: "campaigns", summary: "Launch readiness without launching: blockers grouped by problem, warnings, and the fingerprint `launch` requires.", cli: "dench campaign launch --check", method: "GET", path: "/campaigns/{sequenceId}/launch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:previewLaunch"), }), op({ id: "campaigns.launch", group: "campaigns", summary: "Launch a ready draft. Pass the fingerprint from the preview and acknowledge the LinkedIn notice when LinkedIn steps exist.", cli: "dench campaign launch", method: "POST", path: "/campaigns/{sequenceId}/launch", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:launch"), }), op({ id: "campaigns.pause", group: "campaigns", summary: "Pause an active campaign.", cli: "dench campaign pause", method: "POST", path: "/campaigns/{sequenceId}/pause", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:pauseSequence"), }), op({ id: "campaigns.resume", group: "campaigns", summary: "Resume a paused campaign.", cli: "dench campaign resume", method: "POST", path: "/campaigns/{sequenceId}/resume", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:resumeSequence"), }), op({ id: "campaigns.archive", group: "campaigns", summary: "Archive a campaign and stop everyone still in flight. This is the app's Delete: campaigns are never erased, and their people, leads and activity stay.", cli: "dench campaign archive", aliases: ["dench campaign delete"], method: "POST", path: "/campaigns/{sequenceId}/archive", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:archiveSequence"), }), op({ id: "campaigns.events", group: "campaigns", summary: "The campaign's activity log, newest first (sent, accepted, replied, skipped, failed, paused, stopped).", cli: "dench campaign events", method: "GET", path: "/campaigns/{sequenceId}/events", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:listSequenceEvents"), }), op({ id: "campaigns.people.stop", group: "campaigns", summary: "Stop one person: nothing more is sent to them from this campaign.", cli: "dench campaign stop", method: "POST", path: "/campaigns/people/{enrollmentId}/stop", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:stopEnrollment"), }), op({ id: "campaigns.people.resume", group: "campaigns", summary: "Resume one paused person in an active campaign.", cli: "dench campaign resume-person", method: "POST", path: "/campaigns/people/{enrollmentId}/resume", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/outreachSequences:resumeEnrollment"), }), op({ id: "campaigns.people.setEmail", group: "campaigns", summary: "Give a person in a draft campaign a new or corrected email address; their linked CRM record gets it too. Only before launch; another person in the campaign can't share it.", cli: "dench campaign person set --email", method: "PUT", path: "/campaigns/people/{enrollmentId}/email", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:updateEnrollmentEmail", ), }), op({ id: "campaigns.people.setLinkedin", group: "campaigns", summary: "Give a person in a draft campaign a new or corrected LinkedIn profile URL; their linked CRM record gets it too. Only before launch.", cli: "dench campaign person set --linkedin-url", method: "PUT", path: "/campaigns/people/{enrollmentId}/linkedin", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:updateEnrollmentLinkedin", ), }), op({ id: "campaigns.people.setField", group: "campaigns", summary: "Fill or fix one {{merge field}} for a person in a draft campaign, e.g. a missing Company that blocks launch; a matching column on their CRM record is updated too. Only before launch; written personalized lines are not merge fields and can't be set here.", cli: "dench campaign person set --field", method: "PUT", path: "/campaigns/people/{enrollmentId}/fields", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:updateEnrollmentMergeField", ), }), op({ id: "campaigns.people.resumeAll", group: "campaigns", summary: "Resume every paused person in an active campaign at once, e.g. after a sending account is reconnected. Returns how many were resumed.", cli: "dench campaign resume-person --all", method: "POST", path: "/campaigns/{sequenceId}/people/resume", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:resumePausedEnrollments", ), }), op({ id: "campaigns.people.sync", group: "campaigns", summary: "Fill each unfinished person's empty email, LinkedIn URL, name and merge fields from their linked CRM record, e.g. after enriching the records. Never overwrites a value the campaign already has; returns how many people changed.", cli: "dench campaign sync", method: "POST", path: "/campaigns/{sequenceId}/people/sync", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:syncEnrollmentsFromRecords", ), }), op({ id: "campaigns.people.forRecord", group: "campaigns", summary: "The campaigns a CRM record (a person or a lead) is in, newest first, with the person's status in each and the accounts that send to them.", cli: "dench campaign where", method: "GET", path: "/campaigns/by-record/{entryId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "query", "functions/outreachSequences:listEnrollmentsForEntry", ), }), op({ id: "campaigns.people.timeline", group: "campaigns", summary: "Everything a campaign did for one person, step by step: what was sent (subject and text), whether it was opened or clicked, what was skipped and why, waits, failures and retries.", cli: "dench campaign person show", method: "GET", path: "/campaigns/people/{enrollmentId}/timeline", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/outreachSequences:timeline"), }), op({ id: "campaigns.people.remove", group: "campaigns", summary: "Remove a person who was never contacted from a draft campaign. Frees their place in the 300-person cap and keeps the draft launchable; stop anyone already contacted instead.", cli: "dench campaign remove", method: "DELETE", path: "/campaigns/people/{enrollmentId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/outreachSequences:removeDraftEnrollment", ), }), // Routines. op({ id: "cron.list", group: "routines", summary: "List routines.", cli: "dench cron list", aliases: ["dench routine list", "dench routines list"], method: "GET", path: "/routines", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/triggers:listCronJobs"), }), op({ id: "cron.history", group: "routines", summary: "List routine run history.", cli: "dench cron history", method: "GET", path: "/routines/history", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/triggers:listOrgCronRunHistory"), }), op({ id: "cron.get", group: "routines", summary: "Get a routine.", cli: "dench cron get", method: "GET", path: "/routines/{jobId}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/triggers:getCronJob"), }), op({ id: "cron.create", group: "routines", summary: "Create a routine.", cli: "dench cron create", method: "POST", path: "/routines", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/triggers:createCronTrigger"), }), op({ id: "cron.update", group: "routines", summary: "Update a routine.", cli: "dench cron update", method: "PATCH", path: "/routines/{jobId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/triggers:updateCronTrigger"), }), op({ id: "cron.delete", group: "routines", summary: "Delete a routine.", cli: "dench cron delete", method: "DELETE", path: "/routines/{jobId}", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("cronDelete", "bearer"), }), op({ id: "cron.enable", group: "routines", summary: "Enable a routine.", cli: "dench cron enable", method: "POST", path: "/routines/{jobId}/enable", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/triggers:setCronEnabled"), }), op({ id: "cron.disable", group: "routines", summary: "Disable a routine.", cli: "dench cron disable", method: "POST", path: "/routines/{jobId}/disable", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/triggers:setCronEnabled"), }), op({ id: "cron.run", group: "routines", summary: "Run a routine now.", cli: "dench cron run", method: "POST", path: "/routines/{jobId}/run", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("cronRunNow", "bearer"), }), op({ id: "cron.runs", group: "routines", summary: "List runs for a routine.", cli: "dench cron runs", method: "GET", path: "/routines/{jobId}/runs", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/triggers:listCronRuns"), }), // Files. op({ id: "files.list", group: "files", summary: "List workspace files.", cli: "dench files ls", method: "GET", path: "/files", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/files:listTree", "bearer", { tokenArg: "apiKey", agentSessionAsApiKey: true, }), }), op({ id: "files.content", group: "files", summary: "Read a workspace file's text.", cli: "dench files cat", method: "GET", path: "/files/content", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/files:getFileContent", "bearer", { tokenArg: "apiKey", agentSessionAsApiKey: true, }), }), op({ id: "files.write", group: "files", summary: "Write a workspace file's text.", cli: "dench files write", method: "PUT", path: "/files/content", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/files:editFileFromUI", "bearer", { tokenArg: "apiKey", }), }), op({ id: "files.downloadUrl", group: "files", summary: "Get a signed file download URL.", cli: "dench files download", method: "GET", path: "/files/download-url", requestSchema: noBody, responseSchema: anyResponse, backend: convex( "action", "functions/files:getFileSignedDownloadUrl", "bearer", { tokenArg: "apiKey", agentSessionAsApiKey: true }, ), }), op({ id: "files.delete", group: "files", summary: "Remove a workspace file or directory.", cli: "dench files rm", method: "DELETE", path: "/files", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/files:deleteFile", "bearer", { tokenArg: "apiKey", }), }), op({ id: "files.move", group: "files", summary: "Move a workspace file or directory.", cli: "dench files mv", method: "POST", path: "/files/move", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("filesMove", "bearer"), }), op({ id: "files.mkdir", group: "files", summary: "Create a workspace directory (and any missing parents). Idempotent.", cli: "dench files mkdir", method: "POST", path: "/files/mkdir", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/files:recordDirCreate", "bearer", { tokenArg: "apiKey", }), }), op({ id: "files.stage", group: "files", summary: "Stage uploaded content into the workspace.", cli: "dench stage", method: "POST", path: "/files/stage", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("filesStage", "bearer"), }), op({ id: "fs.daemon", group: "local", summary: "Run the local file sync daemon.", cli: "dench fs", method: "POST", path: "/local/fs", requestSchema: anyObject, responseSchema: anyResponse, backend: local( "The fs daemon watches the caller's local /workspace mount.", ), }), // Gateway-backed commands. op({ id: "search.web", group: "gateway", summary: "Search the web through the Dench gateway.", cli: "dench search", method: "POST", path: "/search", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/search"), }), op({ id: "search.contents", group: "gateway", summary: "Fetch page contents through the Dench gateway.", cli: "dench search contents", method: "POST", path: "/search/contents", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/search/contents"), }), op({ id: "search.answer", group: "gateway", summary: "Answer a question with web search.", cli: "dench search answer", method: "POST", path: "/search/answer", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/search/answer"), }), op({ id: "web.scrape", group: "gateway", summary: "Read a page as clean Markdown through Dench web.", cli: "dench web scrape", method: "POST", path: "/context/web-scrape-markdown", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-scrape-markdown"), }), op({ id: "web.brand", group: "gateway", summary: "Look up a brand kit from a domain or email.", cli: "dench web brand", method: "POST", path: "/context/brand-retrieve", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/brand-retrieve"), }), op({ id: "web.sitemap", group: "gateway", summary: "List sitemap URLs for a domain.", cli: "dench web sitemap", method: "POST", path: "/context/web-sitemap", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-sitemap"), }), op({ id: "web.crawl", group: "gateway", summary: "Crawl a site to Markdown (capped).", cli: "dench web crawl", method: "POST", path: "/context/web-crawl", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-crawl"), }), op({ id: "web.screenshot", group: "gateway", summary: "Capture a rendered page screenshot.", cli: "dench web screenshot", method: "POST", path: "/context/web-screenshot", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-screenshot"), }), op({ id: "web.extract", group: "gateway", summary: "Extract typed fields from a page using a JSON Schema.", cli: "dench web extract", method: "POST", path: "/context/web-extract", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-extract"), }), op({ id: "web.product", group: "gateway", summary: "Extract a product listing from a URL.", cli: "dench web product", method: "POST", path: "/context/web-product", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-product"), }), op({ id: "web.competitors", group: "gateway", summary: "Find competitors of a domain.", cli: "dench web competitors", method: "POST", path: "/context/web-competitors", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/web-competitors"), }), op({ id: "web.styleguide", group: "gateway", summary: "Read a brand styleguide from a domain.", cli: "dench web styleguide", method: "POST", path: "/context/brand-styleguide", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/brand-styleguide"), }), op({ id: "web.fonts", group: "gateway", summary: "Read site fonts from a domain.", cli: "dench web fonts", method: "POST", path: "/context/brand-fonts", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/brand-fonts"), }), op({ id: "web.classify", group: "gateway", summary: "Classify a domain with NAICS or SIC.", cli: "dench web classify", method: "POST", path: "/context/classify-naics", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/classify-naics"), }), op({ id: "web.brandSearch", group: "gateway", summary: "Autocomplete brand names.", cli: "dench web brand-search", method: "POST", path: "/context/brand-search", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/context/brand-search"), }), op({ id: "image.generate", group: "gateway", summary: "Generate an image through the Dench gateway.", cli: "dench image generate", method: "POST", path: "/images/generations", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/images/generations"), }), op({ id: "image.edit", group: "gateway", summary: "Edit an image through the Dench gateway.", cli: "dench image edit", method: "POST", path: "/images/edits", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/images/edits"), }), op({ id: "integrations.list", group: "gateway", summary: "List every connected integration — connected apps plus the Gmail, " + "Outlook, Calendar, LinkedIn and Slack connections Dench provides " + "itself (Slack rows report source: dench).", cli: "dench integrations", method: "GET", path: "/integrations", requestSchema: noBody, responseSchema: anyResponse, backend: custom("connectionsList", "bearer"), }), op({ id: "apps.list", group: "apps", summary: "List installed App Store apps in the workspace.", cli: "dench apps list", method: "GET", path: "/apps", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/appStore:listInstalled"), }), op({ id: "apps.gallery", group: "apps", summary: "List Discover listings — Official (Dench) + Community shelves.", cli: "dench apps gallery", method: "GET", path: "/apps/gallery", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/appStore:listGallery"), }), op({ id: "apps.create", group: "apps", summary: "Scaffold and register a new App Store app.", cli: "dench apps create", method: "POST", path: "/apps", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/appStore:createApp"), }), op({ id: "apps.install", group: "apps", summary: "Install an App Store listing into the workspace.", cli: "dench apps install", method: "POST", path: "/apps/install", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/appStore:install"), }), op({ id: "apps.publish", group: "apps", summary: "Publish an app's files as a versioned App Store listing.", cli: "dench apps publish", method: "POST", path: "/apps/publish", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("action", "functions/appStore:publish"), }), op({ id: "apps.uninstall", group: "apps", summary: "Uninstall an app from the workspace.", cli: "dench apps uninstall", method: "DELETE", path: "/apps/{slug}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/appStore:uninstall"), }), op({ id: "apps.setEnabled", group: "apps", summary: "Enable or disable an installed app.", cli: "dench apps enable", method: "PATCH", path: "/apps/{slug}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/appStore:setEnabled"), }), op({ id: "apps.setPinned", group: "apps", summary: "Pin or unpin an installed app in the rail.", cli: "dench apps pin", method: "POST", path: "/apps/{slug}/pin", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/appStore:setPinned"), }), op({ id: "apps.validate", group: "apps", summary: "Validate a .dench.app folder: manifest, permissions, and every window.dench call it makes.", cli: "dench apps validate", method: "POST", path: "/local/apps/validate", requestSchema: anyObject, responseSchema: anyResponse, backend: local( "Validation reads the app folder on the caller's disk. In the browser, the same catalog is window.dench.api.operations().", ), }), op({ id: "apps.webhooks.register", group: "apps", summary: "Register an inbound webhook for an app. Returns the URL and signing secret ONCE.", cli: "dench apps webhooks register", method: "POST", path: "/apps/{slug}/webhooks", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/appWebhooks:register"), }), op({ id: "apps.webhooks.list", group: "apps", summary: "List an app's registered webhooks (URLs, never secrets).", cli: "dench apps webhooks list", method: "GET", path: "/apps/{slug}/webhooks", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/appWebhooks:list"), }), op({ id: "apps.webhooks.poll", group: "apps", summary: "Read events an app webhook received since a timestamp.", cli: "dench apps webhooks poll", method: "GET", path: "/apps/{slug}/webhooks/{hookName}/events", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("query", "functions/appWebhooks:poll"), }), op({ id: "apps.webhooks.delete", group: "apps", summary: "Remove an app webhook and every event it stored.", cli: "dench apps webhooks delete", method: "DELETE", path: "/apps/{slug}/webhooks/{hookName}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/appWebhooks:remove"), }), op({ id: "tool.status", group: "gateway", summary: "List connected tool accounts.", cli: "dench tool status", method: "GET", path: "/tools/status", requestSchema: noBody, responseSchema: anyResponse, backend: custom("connectionsList", "bearer"), }), op({ id: "tool.connect", group: "gateway", summary: "Create a tool OAuth connection link. Slack is in-house, never " + "a third-party connection: for toolkit slack the CLI prints the first-party install " + "URL (/api/slack/install?slug=, org admin) instead of " + "calling this endpoint.", cli: "dench tool connect", method: "POST", path: "/tools/connections", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("nativeToolConnect", "bearer"), }), op({ id: "tool.search", group: "gateway", summary: "Search for external tool actions.", cli: "dench tool search", method: "POST", path: "/tools/search", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("nativeToolSearch", "bearer"), }), op({ id: "tool.run", group: "gateway", summary: "Run an external tool action. SLACK_* slugs (SLACK_SEND_MESSAGE " + "with Block Kit blocks/buttons, SLACK_FETCH_HISTORY, " + "SLACK_LIST_CHANNELS, …) run through Dench's in-house Slack app " + "install — any channel in the connected workspace — and are always " + "preferred over any third-party Slack connection.", cli: "dench tool run", method: "POST", path: "/tools/run", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("nativeToolRun", "bearer"), }), op({ id: "tool.disconnect", group: "gateway", summary: "Disconnect an external tool account.", cli: "dench tool disconnect", method: "DELETE", path: "/tools/connections/{connectionId}", requestSchema: anyObject, responseSchema: anyResponse, // Proxies the same gateway route, then drops the account's label; the // name lives in Convex and would otherwise outlive the connection. backend: custom("toolDisconnect", "bearer"), }), // Composio reports no account identity for most toolkits, so a workspace // with three Gmail accounts sees three identical rows. These let a human // name a connection once; the label is presentational and grants nothing. op({ id: "tool.labels.list", group: "gateway", summary: "List human names given to connected accounts.", cli: "dench tool labels", method: "GET", path: "/tools/labels", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/composioConnectionLabels:list"), }), op({ id: "tool.label.set", group: "gateway", summary: "Name a connected account, or clear its name with an empty string.", cli: "dench tool label", method: "PUT", path: "/tools/connections/{connectionId}/label", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/composioConnectionLabels:set"), }), // Browser automation (Dench Browser). The provider stays server-side; the // per-(org,user) browser profile is resolved by the gateway, so callers never // see or send provider credentials or context ids. op({ id: "browser.action", group: "browser", summary: "Drive Dench Browser with a single interactive action (open, inspect, click, fill, type, screenshot, stop). Pass the step in `action` plus its arguments.", cli: "dench browser", method: "POST", path: "/browser/cli", requestSchema: anyObject, responseSchema: anyResponse, backend: custom("browserAction", "bearer"), }), op({ id: "browser.session.create", group: "browser", summary: "Create a Dench Browser session and return a CDP connect URL for Playwright (chromium.connectOverCDP) plus a live-view URL. Use for durable scripts run from the sandbox.", cli: "dench browser session create", method: "POST", path: "/browser/sessions", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("POST", "/browser/sessions"), }), op({ id: "browser.session.status", group: "browser", summary: "Get the status, connect URL, and live-view URL for a Dench Browser session.", cli: "dench browser session status", method: "GET", path: "/browser/sessions/{sessionId}", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("GET", "/browser/sessions/{sessionId}"), }), op({ id: "browser.session.stop", group: "browser", summary: "Stop a Dench Browser session and release the remote browser.", cli: "dench browser session stop", method: "DELETE", path: "/browser/sessions/{sessionId}", requestSchema: anyObject, responseSchema: anyResponse, backend: gateway("DELETE", "/browser/sessions/{sessionId}"), }), // Agent harness config. op({ id: "agentConfig.identity.show", group: "agent-config", summary: "Read IDENTITY.md.", cli: "dench context --sections identity", aliases: ["dench identity show"], method: "GET", path: "/agent-config/identity", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.identity.edit", group: "agent-config", summary: "Update IDENTITY.md.", cli: "dench context set identity", aliases: ["dench identity edit"], method: "PUT", path: "/agent-config/identity", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:updateIdentity"), }), op({ id: "agentConfig.organisation.show", group: "agent-config", summary: "Read ORGANISATION.md.", cli: "dench context --sections organisation", aliases: ["dench organisation show"], method: "GET", path: "/agent-config/organisation", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.organisation.edit", group: "agent-config", summary: "Update ORGANISATION.md.", cli: "dench context set organisation", aliases: ["dench organisation edit"], method: "PUT", path: "/agent-config/organisation", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:updateOrganisation"), }), op({ id: "agentConfig.user.show", group: "agent-config", summary: "Read USER.md.", cli: "dench context --sections user", aliases: ["dench user show"], method: "GET", path: "/agent-config/user", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.user.edit", group: "agent-config", summary: "Update USER.md.", cli: "dench context set user", aliases: ["dench user edit"], method: "PUT", path: "/agent-config/user", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:updateUserProfile"), }), op({ id: "agentConfig.tools.show", group: "agent-config", summary: "Read tool notes.", cli: "dench context --sections tools", aliases: ["dench tools show"], method: "GET", path: "/agent-config/tools", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.tools.edit", group: "agent-config", summary: "Update tool notes.", cli: "dench context set tools", aliases: ["dench tools edit"], method: "PUT", path: "/agent-config/tools", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:updateToolsNotes"), }), op({ id: "workspace.context.rules.set", group: "workspace", summary: "Update standing workspace rules; credentials belong in secrets and runnable setup in environments.", cli: "dench context set rules", method: "PUT", path: "/context/rules", requestSchema: z.object({ content: z.string().min(1).max(32000) }).strict(), responseSchema: anyResponse, backend: { ...convex("mutation", "functions/agentConfig:writeMagicFileFromUI"), fixedArgs: { path: "/RULES.md" }, }, }), op({ id: "agentConfig.mem.show", deprecated: true, group: "agent-config", summary: "Read aggregate memory.", cli: "dench mem show", method: "GET", path: "/agent-config/mem", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.mem.edit", deprecated: true, group: "agent-config", summary: "Update aggregate memory.", cli: "dench mem set", method: "PUT", path: "/agent-config/mem", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:updateMemory"), }), op({ id: "agentConfig.bootstrap.show", deprecated: true, group: "agent-config", summary: "Read bootstrap state.", cli: "dench bootstrap status", method: "GET", path: "/agent-config/bootstrap", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.bootstrap.complete", deprecated: true, group: "agent-config", summary: "Complete bootstrap.", cli: "dench bootstrap complete", method: "POST", path: "/agent-config/bootstrap/complete", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:completeBootstrap"), }), op({ id: "agentConfig.bootstrap.template", deprecated: true, group: "agent-config", summary: "Update bootstrap template.", cli: "dench bootstrap template", method: "PUT", path: "/agent-config/bootstrap/template", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/agentConfig:updateBootstrapTemplate", ), }), op({ id: "agentConfig.model.default", group: "agent-config", summary: "Set the default chat model.", cli: "dench model default", method: "PUT", path: "/agent-config/model/default", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:setDefaultChatModelV2"), }), op({ id: "agentConfig.model.thread", group: "agent-config", summary: "Set a thread model override.", cli: "dench model thread", method: "PUT", path: "/agent-config/model/threads/{threadId}", requestSchema: anyObject, responseSchema: anyResponse, backend: convex("mutation", "functions/agentConfig:setThreadModelOverride"), }), op({ id: "agentConfig.daily.today", deprecated: true, group: "agent-config", summary: "Read today's daily activity path.", cli: "dench daily today", method: "GET", path: "/agent-config/daily/today", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), op({ id: "agentConfig.daily.show", deprecated: true, group: "agent-config", summary: "Read a daily activity path.", cli: "dench daily show", method: "GET", path: "/agent-config/daily/{date}", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/agentConfig:getAgentConfig"), }), // Notifications — the signed-in member's inbox (the bell). Agent sessions // read their owner's inbox; an org API key with no person behind it is // refused by the backend. op({ id: "notifications.list", group: "notifications", summary: "List your notifications (inbox, unread, archived, or all).", cli: "dench notifications list", method: "GET", path: "/notifications", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/notifications:listMyNotifications"), }), op({ id: "notifications.unreadCount", group: "notifications", summary: "How many unread notifications you have.", cli: "dench notifications count", method: "GET", path: "/notifications/unread-count", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/notifications:unreadCount"), }), op({ id: "notifications.read", group: "notifications", summary: "Mark a notification read (or unread).", cli: "dench notifications read", method: "PATCH", path: "/notifications/{notificationId}", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex("mutation", "functions/notifications:setNotificationRead"), transformArgs: (input) => ({ notificationId: input.notificationId, read: input.read !== false, }), }, }), op({ id: "notifications.readAll", group: "notifications", summary: "Mark every inbox notification read.", cli: "dench notifications read-all", method: "POST", path: "/notifications/read-all", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/notifications:markAllRead"), }), op({ id: "notifications.archive", group: "notifications", summary: "Archive a notification.", cli: "dench notifications archive", method: "POST", path: "/notifications/{notificationId}/archive", requestSchema: noBody, responseSchema: anyResponse, backend: convex("mutation", "functions/notifications:archiveNotification"), }), op({ id: "notifications.preferences.get", group: "notifications", summary: "Read your notification preferences.", cli: "dench notifications prefs", method: "GET", path: "/notifications/preferences", requestSchema: noBody, responseSchema: anyResponse, backend: convex( "query", "functions/notifications:getMyNotificationPreferences", ), }), op({ id: "notifications.preferences.update", group: "notifications", summary: "Update your notification preferences.", cli: "dench notifications prefs --set", method: "PATCH", path: "/notifications/preferences", requestSchema: anyObject, responseSchema: anyResponse, backend: convex( "mutation", "functions/notifications:updateMyNotificationPreferences", ), }), // Workspace settings — Settings → Sync: the eight switches for what Dench // does on its own with email, calendar, Slack and LinkedIn. Reads for any // member; the write is admin-only server-side. op({ id: "settings.sync.get", group: "settings", summary: "Read the workspace's Sync switches (accounts from email, contacts from LinkedIn, Slack Connect, calendar, dossier, tasks from email, lead auto-qualify, summaries and Brief). All on by default.", cli: "dench settings sync", method: "GET", path: "/settings/sync", requestSchema: noBody, responseSchema: anyResponse, backend: convex("query", "functions/organizations:getMySyncSettings"), }), op({ id: "settings.sync.update", group: "settings", summary: "Flip Sync switches (admin). Partial patch: only the keys sent change. Off stops new work; nothing already created is removed.", cli: "dench settings sync set", method: "PATCH", path: "/settings/sync", requestSchema: anyObject, responseSchema: anyResponse, backend: { ...convex( "mutation", "functions/organizations:updateMyOrganizationSyncSettings", ), transformArgs: (input) => ({ patch: input }), }, }), ] as const satisfies readonly ApiOperation[]; /** * Final operation list with deep schemas + backend extras injected from * `api-schemas.ts` by operation id. This keeps the literal above readable * while the parameter-level depth (every field, enum, schedule, etc.) * lives in one schema module shared by the dispatcher, OpenAPI generator, * and curl guide. */ export const apiOperations: ApiOperation[] = rawApiOperations.map( (operation) => { const requestSchema = requestSchemas[operation.id] ?? operation.requestSchema; const responseSchema = responseSchemas[operation.id] ?? operation.responseSchema; let backend: ApiOperationBackend = operation.backend; if (backend.kind === "convex") { const extras = convexExtras[operation.id]; if (extras) backend = { ...backend, ...extras }; } return { ...operation, requestSchema, responseSchema, backend }; }, ); export function operationRequiresBearer(operation: ApiOperation): boolean { return ( (operation.backend.kind === "convex" && operation.backend.auth === "bearer") || (operation.backend.kind === "custom" && operation.backend.auth === "bearer") || operation.backend.kind === "gateway" ); } export function publicApiOperations(): ApiOperation[] { return apiOperations.filter( (operation) => operation.backend.kind !== "local", ); }