import type { McpScope } from "@prisma/client"; /** * The manual's sections, and the four jobs built out of them. * * Two things need these boundaries now — the manual's pages and a connection's * scope — and two copies of a range table is a drift waiting for a new tool to * land in the wrong one. `tools/gen-tool-docs.mjs` keeps only what is about * pages (file, title, icon, blurb) and asserts its keys against this list. * * Boundaries are first/last tool NAMES rather than indexes, so reordering the * array is caught rather than silently reshuffling both the manual and every * scope. * * Both literals below are PLAIN DATA — no function calls, no template strings, * no spreads — because tools/tool-source.mjs evaluates them as text. Keep them * that way. * * This file is not in src/lib/data/: it touches no database and takes no * userId, the same way src/lib/pipeline-fields.ts and src/lib/column-widths.ts * do not. It must stay importable by tools.ts with NO import back the other * way, or the module graph cycles. */ export const SECTIONS = [ { key: "me", first: "search_me", last: "import_linkedin_archive" }, { key: "resumes", first: "get_resume_format", last: "check_resume_fit" }, { key: "letters", first: "prep_letter", last: "delete_letter" }, { key: "pipeline", first: "pipeline_stats", last: "list_field_values" }, { key: "crm", first: "list_companies", last: "schedule_contact_pings" }, { key: "archive", first: "list_archive", last: "empty_archive" }, { key: "accounts", first: "list_linked_accounts", last: "search_calendar" }, { key: "connections", first: "get_digest_settings", last: "delete_connection" }, { key: "admin", first: "admin_instance_stats", last: "admin_delete_variable" }, ] as const; export type SectionKey = (typeof SECTIONS)[number]["key"]; /** * The four tools every scope carries, whatever else it does not. * * Small on purpose, and read-only to a tool. The briefing names `search_me` as * the way in and would be lying in three scopes out of four without it; * `whoami` is how a narrowed client explains ITSELF — it returns the * connection's scope — which is the difference between "I can't do that" and * "that tool isn't served here, and here is how your person widens it", and its * `setup` is what a first conversation opens with; `list_connections` names * the other connections. Nothing that writes is ever core — which is * what makes READONLY's core fall out for free rather than needing an * exception. */ export const CORE_TOOLS = ["search_me", "whoami", "list_connections"] as const; export const SCOPES = [ { key: "FULL", label: "Everything", /** Empty sections means "no filter at all", not "no sections". */ sections: [], extras: [], readOnly: false, blurb: "Every tool this account can reach. The default, and what every connection had before scopes existed.", }, { key: "WRITING", label: "Me, resumes and letters", sections: ["me", "resumes", "letters"], /** * A document is aimed at a job at a company, and tailor_resume_for_application, * prep_letter and create_letter all take an applicationId somebody has to * look up first. get_company is already in ALWAYS_LOAD for the same reason. */ extras: [ "list_applications", "get_application", "list_companies", "get_company", // The undo path. These are filed under connections because that is where // the change log lives, but they are about WRITES — and a scope whose // whole job is writing roles and resumes, with no way back from an // update_role that ate somebody's background, is the exact footgun they // were built for. `--scope-audit` in tools/eval-tool-choice.mjs is what // found them sitting in FULL alone. "list_changes", "list_revisions", "restore_revision", ], readOnly: false, blurb: "Writes documents and the material behind them. Cannot move the pipeline, tag anybody or delete a company.", }, { key: "PIPELINE", label: "The search", sections: ["pipeline", "crm", "archive", "accounts"], /** * list_resumes answers "which one did I send them" without opening the * resume tools; preview_digest is a pipeline read that happens to be filed * on the account page. The change log is here so this scope can say what * it did. restore_revision is NOT: nothing this scope writes is versioned * — only resumes and roles are — so there is nothing here for it to undo. */ extras: ["list_resumes", "preview_digest", "list_changes"], readOnly: false, blurb: "Runs the search: applications, offers, interviews, people, tasks, the archive, mail and calendar.", }, { key: "READONLY", /** * Every section, filtered to what writes nothing. Admin is excluded like * every other non-FULL scope. */ label: "Read-only", sections: ["me", "resumes", "letters", "pipeline", "crm", "archive", "accounts", "connections"], extras: [], readOnly: true, blurb: "Reads anything, writes nothing. For a client you want looking but not touching.", }, ] as const; export const SCOPE_VALUES = ["FULL", "WRITING", "PIPELINE", "READONLY"] as const; export const scopeByKey = new Map(SCOPES.map((scope) => [scope.key, scope])); export function scopeLabel(scope: McpScope): string { return scopeByKey.get(scope)?.label ?? "Everything"; } export function scopeBlurb(scope: McpScope): string { return scopeByKey.get(scope)?.blurb ?? ""; }