UNPKG

@extenshi/mcp

Version:

Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools

182 lines • 8.48 kB
/** * Extension templates — the "what kind of extension is this?" taxonomy. * * A template is not a folder of files to copy. It is a small set of claims * about the extension's shape: which manifest components it cannot work * without, which permissions those components force, and which files the * scaffold has to write so the manifest does not point at something that isn't * there. Everything downstream — the picker in the manifest generator, the * locked checkboxes, the scaffolded file set, and (next phase) the MCP tool * that hands this catalog to a developer's own agent — reads THIS module. * * It lives in shared-types rather than in dojo because the readers span * packages: the website, catalog-bff (answering a developer's own agent), and * the published `@extenshi/mcp` package, whose tool tells an agent what kinds of * extension exist and what each costs in permissions. All three must give the * same answer. * * SELF-CONTAINED ON PURPOSE — no imports, so it can be copied verbatim into * `@extenshi/mcp`, which cannot depend on unpublished workspace packages (same * constraint and same solution as welcome-agent-brief.ts). * * SINGLE SOURCE, TWO COPIES, byte-identical: * shared-types/extension-templates.ts ← canonical * tools/extenshi-mcp/src/extension-templates.ts ← copy, pinned by a test * Edit the canonical file and copy it over; the sync test in the MCP package * fails on any divergence inside the monorepo and skips in the npm tarball. * * Templates COMBINE. A side panel with a content script is one extension with * two capabilities, not a fifth template — the picker is multi-select and every * helper here takes a list and unions the result. That is why requirements are * expressed as components + permissions rather than as whole manifests. */ export const EXTENSION_TEMPLATE_IDS = ['popup', 'sidepanel', 'content', 'overlay']; /** * The template applied when a project has never picked one. * * `popup` is deliberately the same shape the scaffold produced before templates * existed, so an old project regenerates byte-for-byte what it did yesterday. */ export const DEFAULT_EXTENSION_TEMPLATE = 'popup'; /** * Manifest components a template can require. * * These are the things the UI can lock: a side-panel extension whose * `side_panel` key is turned off is not a side-panel extension, it is a broken * one. Each maps to a group of fields in the manifest draft. */ export const EXTENSION_TEMPLATE_COMPONENTS = ['action', 'sidePanel', 'contentScript']; /** Where scaffolded source goes, so the manifest and the file writer agree. */ export const EXTENSION_TEMPLATE_SOURCE_DIR = 'src'; /** * The background service worker every template gets, template or not. * * It is not part of any one template because it carries the integration the * whole product is built around: `setUninstallURL` must be registered at * startup, and a template that skipped the background script would silently * ship an extension that can never report why someone uninstalled it. */ export const EXTENSION_BACKGROUND_PATH = `${EXTENSION_TEMPLATE_SOURCE_DIR}/background.js`; export const EXTENSION_TEMPLATE_CATALOG = [ { id: 'popup', label: 'Popup', summary: 'Opens a small window from the toolbar icon. The default shape for a self-contained tool.', requires: ['action'], // A popup needs nothing. Saying so is the point: our own store-compliance // advice is "ask for the minimum", and a starter that pads the manifest to // look substantial teaches the opposite on day one. requiredPermissions: [], recommendedPermissions: ['storage'], entry: `${EXTENSION_TEMPLATE_SOURCE_DIR}/popup.html`, emits: [`${EXTENSION_TEMPLATE_SOURCE_DIR}/popup.html`], }, { id: 'sidepanel', label: 'Side panel', summary: 'Opens a panel docked beside the page, which stays open while the user browses.', requires: ['sidePanel', 'action'], requiredPermissions: ['sidePanel'], recommendedPermissions: [], entry: `${EXTENSION_TEMPLATE_SOURCE_DIR}/sidepanel.html`, emits: [ `${EXTENSION_TEMPLATE_SOURCE_DIR}/sidepanel.html`, `${EXTENSION_TEMPLATE_SOURCE_DIR}/sidepanel.js`, ], }, { id: 'content', label: 'Page enhancer', summary: 'Runs inside specific sites you list and changes what is already on the page.', requires: ['contentScript'], requiredPermissions: [], recommendedPermissions: [], entry: `${EXTENSION_TEMPLATE_SOURCE_DIR}/content.js`, emits: [`${EXTENSION_TEMPLATE_SOURCE_DIR}/content.js`], defaultMatches: ['https://example.com/*'], }, { id: 'overlay', label: 'In-page assistant', summary: 'Adds your own UI on top of any site — a form filler, a password helper, a clipper.', requires: ['contentScript'], requiredPermissions: [], // An assistant that cannot remember anything between pages is a demo, so // storage is ticked — but it stays removable, because an overlay that only // reformats what is on screen genuinely does not need it. recommendedPermissions: ['storage'], entry: `${EXTENSION_TEMPLATE_SOURCE_DIR}/overlay.js`, emits: [`${EXTENSION_TEMPLATE_SOURCE_DIR}/overlay.js`], defaultMatches: ['<all_urls>'], }, ]; const BY_ID = new Map(EXTENSION_TEMPLATE_CATALOG.map((t) => [t.id, t])); export function isExtensionTemplateId(value) { return typeof value === 'string' && BY_ID.has(value); } export function getExtensionTemplate(id) { const template = BY_ID.get(id); // Unreachable through the type, but this module is also read from persisted // JSON — throwing here beats returning undefined into a manifest builder. if (!template) throw new Error(`Unknown extension template: ${id}`); return template; } /** * Clean a persisted or client-supplied list into templates we know. * * Saved tool state is free-form JSON written by whatever build was live at the * time, so an unknown id is expected rather than exceptional: it is dropped, * not thrown on. Order follows the catalog so two projects that picked the same * set produce the same manifest, and duplicates collapse. */ export function normalizeExtensionTemplates(value) { if (!Array.isArray(value)) return []; const picked = new Set(value.filter(isExtensionTemplateId)); return EXTENSION_TEMPLATE_CATALOG.filter((t) => picked.has(t.id)).map((t) => t.id); } /** The selection to build with: what was picked, or the default when nothing was. */ export function effectiveExtensionTemplates(value) { const templates = normalizeExtensionTemplates(value); return templates.length ? templates : [DEFAULT_EXTENSION_TEMPLATE]; } function unionOf(ids, pick) { const out = []; for (const id of ids) { for (const value of pick(getExtensionTemplate(id))) { if (!out.includes(value)) out.push(value); } } return out; } /** Permissions the selection forces into the manifest (locked in the UI). */ export function extensionTemplateRequiredPermissions(ids) { return unionOf(ids, (t) => t.requiredPermissions); } /** Permissions ticked when a template is picked, minus the ones already forced. */ export function extensionTemplateRecommendedPermissions(ids) { const required = extensionTemplateRequiredPermissions(ids); return unionOf(ids, (t) => t.recommendedPermissions).filter((p) => !required.includes(p)); } /** Components the selection locks on. */ export function extensionTemplateComponents(ids) { const out = []; for (const id of ids) { for (const component of getExtensionTemplate(id).requires) { if (!out.includes(component)) out.push(component); } } return out; } /** Templates whose component is a content script, in catalog order. */ export function extensionTemplatesWithContentScripts(ids) { return ids.map(getExtensionTemplate).filter((t) => t.requires.includes('contentScript')); } /** Every file the scaffold writes for a selection, background script included. */ export function extensionTemplateFiles(ids) { return [EXTENSION_BACKGROUND_PATH, ...unionOf(ids, (t) => t.emits)]; } //# sourceMappingURL=extension-templates.js.map