/** * Plugin authoring engine (spec §3) — shared by the two ProposePlugin tools. * * Completes the discover → acquire → author spectrum: when no marketplace plugin * fits a gap, the model can scaffold one. Authoring is gated on the *content / * capability-grant* trust axis (what the plugin can do), not the *source* axis * used for install. This module carries the risk classification, the * privilege-amplification guardrail, and the file writer; the tools own the two * escalating-risk *paths* (autonomous scaffold vs. confirm-then-activate). * * Everything is written through the format registry's {@link emitForPlatforms}, * so an authored plugin lands in the requested vendor layouts (Claude Code and * GitHub Copilot by default) and round-trips back through {@link parsePluginDir}. */ import type { MarketplacePlatform, PluginDraft } from "./formats/types.js"; import { type NormalizedPlugin } from "./manifest.js"; export { resolvePluginPlatforms } from "./formats/platform-targets.js"; export interface AllowlistClassification { /** read-only grants are as safe as a skill; mutating/exec/network/`*` grants need confirmation. */ risk: "read-only" | "mutating"; /** Human-readable explanation of what drove the classification. */ reason: string; /** Any plugin-system (capability-acquisition) tools found — always forbidden in an authored allowlist. */ pluginTools: string[]; /** The raw allowlist tokens. */ tokens: string[]; } /** * Classify an authored subagent `tools:` allowlist as read-only vs. mutating, * reusing the same Claude-alias vocabulary as the agent-frontmatter normalizer * (spec §3 "compute the risk, don't guess it"). Anything unrecognized — an MCP * tool, a bare `*`, an unknown name — is treated as mutating (fail-safe). */ export declare function classifyAllowlist(tools: string | undefined): AllowlistClassification; export interface WriteResult { /** Where the plugin ended up: a production home, or the draft dir when not promoted. */ dest: string; /** False when the caller asked to stop at the draft (gates still to run). */ promoted: boolean; /** Platforms the draft was emitted for. */ targets: MarketplacePlatform[]; /** Written file paths, relative to the plugin root. */ files: string[]; /** Re-parsed plugin (confirms the scaffold round-trips). */ plugin: NormalizedPlugin | null; } /** Locate an existing plugin by id across every home, or undefined. */ export declare function findPluginDir(cwd: string, id: string): string | undefined; /** Whether the plugin at `id` was authored here (carries the provenance marker), vs. installed or hand-written. */ export declare function isAuthoredPlugin(cwd: string, id: string): boolean; /** * Render `draft` into the requested platform layouts and write it out. * * Two modes, and the distinction matters: * * - **Creation** (default): emit into an ephemeral draft dir, then promote into * the target platform's production home, replacing whatever was there. A * production home is live — `~/.claude/skills//` is loaded by Claude Code * on its next session — so writing there directly would mean a plugin that * failed validation, or whose confirmation was declined, is already running. * Pass `promote: false` to stop at the draft and promote later with * {@link promoteDraft}. * - **In-place edit** (`dest` given): write the emitted files over an existing * plugin directory, wherever it already lives. An edit must never relocate the * plugin — a merge or a removal on a plugin sitting in the consumption home or * the legacy project home would otherwise silently move it into a production * home. Files not named by the emit are left alone, which is what makes the * directory-scanned merge semantics work. */ export declare function writePluginDraft(draft: PluginDraft, platforms?: MarketplacePlatform[], options?: { promote?: boolean; dest?: string; }): WriteResult; /** * Move a draft into the production home of its first target platform, replacing * whatever is there. The draft directory is consumed either way. * * A cross-device rename fails when temp and home sit on different filesystems, * so this falls back to copy-then-delete rather than assuming `rename` works. */ export declare function promoteDraft(draftDir: string, id: string, targets: readonly MarketplacePlatform[]): string; /** * Replace `dest` with the contents of a draft. Used when the destination is * already known — an edit stays wherever the plugin lives, which may be the * consumption home or a legacy directory rather than a production home. */ export declare function promoteDraftTo(draftDir: string, dest: string): string; /** * Whether authoring this id would clobber something **in the target platform's * production home**. * * Scoped to the target rather than every home on purpose: a `claude` `foo` and a * `github` `foo` are artifacts for two different ecosystems, not duplicates of * each other, and a marketplace-installed `foo` in the consumption home is a * third unrelated thing. Only a collision in the directory this write would * actually land in is a collision. */ export declare function pluginExists(id: string, platforms?: readonly MarketplacePlatform[]): boolean; /** Load an installed/authored plugin by id from whichever home holds it, or null. */ export declare function getPlugin(cwd: string, id: string): NormalizedPlugin | null; /** * Merge inline-authored `delta` capabilities into the existing local plugin `id` * and re-emit. Unlike a marketplace `UpdatePlugin`, nothing is fetched from a * remote source — the new content comes from the caller — so the supply-chain * "benign v1 → hostile v2" vector the spec guards against is structurally absent. * * Merge semantics: * - **Skills / commands / agents** are directory-scanned, so existing ones are * left on disk untouched; a delta entry with a matching name overwrites just * that file (an update), a new name is added. * - **Hooks** and **MCP servers** live in single files that a re-emit rewrites, * so they are re-emitted as the *union* of existing + delta (MCP keyed by * server name with delta winning; hooks deduped by event/matcher/command). * Hooks have no name, so there is deliberately no modify-in-place: a delta * hook with the same event/matcher but a different command is a NEW hook * added alongside the old one, never a replacement. (Keying replacement by * event+matcher would silently drop legitimate sibling hooks that share * them.) Changing a hook = {@link removeFromPlugin} the old one + merge the * new one. * - **Metadata** (version, description, author) takes the delta's value when * provided, else keeps the existing one. * * Platforms default to the plugin's existing `supportPlatform` so a merge never * silently adds or drops a vendor layout. */ export declare function mergePluginDraft(cwd: string, id: string, delta: Partial, platforms?: MarketplacePlatform[]): WriteResult; /** A hook to remove: `event` is required; `matcher`/`command` narrow the match when provided. */ interface HookRemovalSpec { event: string; matcher?: string; command?: string; } /** Named capabilities to remove from an authored plugin. */ export interface RemovalSpec { skills?: string[]; commands?: string[]; subagents?: string[]; mcpServers?: string[]; hooks?: HookRemovalSpec[]; } export interface RemoveResult { dest: string; /** Human-readable descriptions of what was removed. */ removed: string[]; /** Requested capabilities that were not found (nothing was removed for these). */ missing: string[]; } /** * Remove named capabilities from the authored plugin `id`. The inverse of the * additive merge, and — like {@link mergePluginDraft} — authored-only. * * Removal is the low-risk direction (deleting capabilities cannot execute * code), which is why callers may run it without a confirmation gate. * * - **Skills / commands / subagents** are directory-scanned, so removal is a * surgical file delete at our emit conventions; no re-emit needed. * - **Hooks** (matched by event, narrowed by matcher/command when given) and * **MCP servers** (by name) live in single files, so the remaining set is * re-emitted — and when a set empties, its file is DELETED, because the * parser falls back to `hooks/hooks.json` / `.mcp.json` on disk and a stale * file would resurrect the removed capability on the next parse. */ export declare function removeFromPlugin(cwd: string, id: string, spec: RemovalSpec): RemoveResult; //# sourceMappingURL=authoring.d.ts.map