plugin

package
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package plugin implements iterion's plugin ecosystem: declarative, out-of-process extensions described by a `plugin.yaml` manifest with typed contribution points. iterion cannot use Go's `plugin` package (it ships static CGO_ENABLED=0 binaries that are bind-mounted into sandbox containers), so a plugin never injects Go code — it declares WHAT to contribute and the runtime wires it into iterion's existing seams:

rewriters   → command-output compressors (the rtk generalization) applied
              on the three shell surfaces (claude_code hook, claw builtin,
              tool node), composable as an ordered chain.
mcp_servers → MCP servers merged into the workflow MCP catalog (the natural
              home for knowledge-graph explorers like repo-falcon).
skills      → markdown skills mirrored into <workspace>/.claude/skills/.
lifecycle   → index/refresh commands (e.g. build/refresh a code graph).

Plugins load from two sources: builtins embedded in the binary (rtk enabled by default; graphify + repo-falcon shipped disabled) and installed plugins under ~/.iterion/plugins/<name>/. Enable/disable state lives in ~/.iterion/plugins.yaml; the marketplace installs third-party plugins into the same directory.

Index

Constants

View Source
const CommandPlaceholder = "{{command}}"

CommandPlaceholder is the token in a rewriter argv replaced by the full shell command line at rewrite time.

View Source
const ManifestFile = "plugin.yaml"

ManifestFile is the manifest filename at a plugin's root.

View Source
const SchemaVersion = 1

SchemaVersion is the current plugin.yaml schema. Unknown future versions are rejected by ParseManifest so an old binary fails loudly rather than silently dropping contributions it cannot honour.

Variables

View Source
var ErrNoManifest = errors.New("plugin: no plugin.yaml")

ErrNoManifest is returned by Inspect when the source directory has no plugin.yaml. Callers branch on it with errors.Is (e.g. the marketplace submit flow falls back to skill-library synthesis).

View Source
var MirrorKinds = []MirrorKind{
	{Name: "skill", Dir: "skills"},
	{Name: "command", Dir: "commands"},
	{Name: "agent", Dir: "agents"},
}

MirrorKinds is the set of markdown contribution kinds, each mirrored into its own .claude/ subdir with the shared collision policy.

Functions

func Install

func Install(ctx context.Context, src string) (string, error)

Install installs a plugin from a local directory or git URL into ~/.iterion/plugins/<name>/ and returns the installed plugin's name. It is InstallWith with only a source. Both the CLI (`iterion plugin install`) and the HTTP server (POST /api/v1/plugins/install) call this so the behaviour is identical on either surface.

func InstallWith added in v0.43.0

func InstallWith(ctx context.Context, opts InstallOptions) (string, error)

InstallWith installs a plugin per opts into ~/.iterion/plugins/<name>/ and returns the installed plugin's name. A git source is shallow-cloned (at opts.Ref when set); a local path's plugin.yaml is validated then copied. When the (sub)source has no plugin.yaml but ships bare skills (a public skill library), a skills-only manifest is synthesized and persisted into the install dir.

InstallWith only places files — it never executes plugin code; the cloned repo's .git metadata is stripped (see copyTree).

func NormalizeName

func NormalizeName(src string) string

NormalizeName derives a kebab-case plugin name from a directory path or git URL: it takes the last path segment, strips a trailing ".git", lowercases, and replaces any run of non-alphanumeric characters with a single dash.

func ReadReadme added in v0.43.0

func ReadReadme(dir string) (string, error)

ReadReadme returns the README body from dir, matching the filename case-insensitively (README.md / readme.md / Readme.md, …) and capping the content at 16 KiB. A directory with no README yields an empty string, not an error; an unreadable directory or file is an error.

func RunLifecycle added in v0.43.0

func RunLifecycle(ctx context.Context, reg *Registry, name, phase, workspace string, stdout, stderr io.Writer) error

RunLifecycle executes a plugin's lifecycle command ("index" or "refresh") in the given workspace (default: cwd), streaming subprocess output to stdout/stderr. Placeholders ({{workspace}}, {{plugin.dir}}, {{plugin.cache}}, {{config.<key>}}) are expanded before the command runs via `sh -c`; the plugin's cache directory is created so {{plugin.cache}} always resolves to an existing path. Shared by the CLI (`iterion plugin run`) and the HTTP server so both surfaces run lifecycles identically.

func Uninstall

func Uninstall(name string) error

Uninstall removes an installed plugin. Builtins cannot be uninstalled (Registry.Remove rejects them — disable instead).

func WriteManifest

func WriteManifest(dir string, m *Manifest) error

WriteManifest marshals m to <dir>/plugin.yaml. Used by the install path to persist a synthesized manifest into an installed skill library.

Types

type ConfigField

type ConfigField struct {
	// Key is the setting id, referenced as {{config.<key>}}.
	Key string `yaml:"key" json:"key"`
	// Label is the studio form label; defaults to Key when empty.
	Label string `yaml:"label" json:"label,omitempty"`
	// Type is one of string | bool | int | float | enum | secret (default string).
	// secret renders as a password field and its value is never sent back to the
	// studio (only whether it's set).
	Type string `yaml:"type" json:"type,omitempty"`
	// Description is shown under the field in the studio.
	Description string `yaml:"description" json:"description,omitempty"`
	// Default is the value used until the operator sets one.
	Default string `yaml:"default" json:"default,omitempty"`
	// Options are the allowed values for type: enum.
	Options []string `yaml:"options" json:"options,omitempty"`
	// Required marks the field as mandatory (advisory; surfaced in the studio).
	Required bool `yaml:"required" json:"required,omitempty"`
}

ConfigField declares one user-configurable setting. Values are stored and expanded as strings (bool/int render as their natural input in the studio but persist as "true"/"30"), which is what {{config.<key>}} substitutes into a plugin's commands and process environment.

type Contributes

type Contributes struct {
	Rewriters  []RewriterSpec  `yaml:"rewriters"`
	MCPServers []MCPServerSpec `yaml:"mcp_servers"`
	Skills     []string        `yaml:"skills"`
	Commands   []string        `yaml:"commands"`
	Agents     []string        `yaml:"agents"`
	// Hooks are paths to JSON settings fragments ({"hooks": {<Event>: [...]}}),
	// idempotently merged into the workspace's .claude/settings.json so
	// claude_code fires them (discovered via --setting-sources project). A
	// command-type hook runs arbitrary shell on tool events — installed plugins
	// are opt-in (disabled by default), so this is the operator's choice.
	Hooks     []string       `yaml:"hooks"`
	Lifecycle *LifecycleSpec `yaml:"lifecycle"`
}

Contributes is the set of typed contribution points.

Skills, Commands and Agents are markdown files mirrored into the workspace's .claude/<skills|commands|agents>/ directory at run start (claude_code discovers them via --setting-sources project; the claw backend reads the same dirs). They share one mirror mechanism and one collision policy.

type Detail added in v0.43.0

type Detail struct {
	View       View            `json:"view"`
	Readme     string          `json:"readme,omitempty"`
	AutoIndex  bool            `json:"auto_index"`
	Rewriters  []RewriterInfo  `json:"rewriters,omitempty"`
	MCPServers []MCPServerInfo `json:"mcp_servers,omitempty"`
	Skills     []string        `json:"skills,omitempty"`
	Commands   []string        `json:"commands,omitempty"`
	Agents     []string        `json:"agents,omitempty"`
	Hooks      []HookInfo      `json:"hooks,omitempty"`
	Lifecycle  *LifecycleInfo  `json:"lifecycle,omitempty"`
	Dir        string          `json:"dir,omitempty"`
}

Detail is the studio-facing full projection of one plugin: the listing View plus README and every contribution spelled out (what a rewriter injects, which MCP servers start, which files mirror, what shell the hooks fire).

type ExpandContext

type ExpandContext struct {
	Workspace string
	PluginDir string
	CacheDir  string
	// Config is the plugin's effective config (defaults overlaid with operator
	// values), exposed as {{config.<key>}} placeholders.
	Config map[string]string
}

ExpandContext carries the values used to expand activation-time placeholders in mcp_servers args/env and lifecycle commands.

func (ExpandContext) Expand

func (e ExpandContext) Expand(s string) string

Expand substitutes {{workspace}}, {{plugin.dir}}, {{plugin.cache}} and {{config.<key>}} in s.

type HookInfo added in v0.43.0

type HookInfo struct {
	Event    string   `json:"event"`
	Commands []string `json:"commands,omitempty"`
}

HookInfo surfaces one hook event contributed by a plugin: the claude_code event name (PreToolUse, Stop, …) and the raw shell command strings the hook fires — the studio shows these verbatim so the operator can vet what an opt-in plugin runs on tool events.

type InspectInfo added in v0.43.0

type InspectInfo struct {
	Manifest *Manifest
	README   string
}

InspectInfo is what Inspect reads out of a plugin source directory before any install: the validated manifest plus the README body (empty when the source ships none).

func Inspect added in v0.43.0

func Inspect(ctx context.Context, srcDir string) (*InspectInfo, error)

Inspect reads and validates a plugin source directory without installing it: parses <srcDir>/plugin.yaml (a missing manifest reports ErrNoManifest) and reads the README via ReadReadme. The context is accepted for API symmetry with Install/InstallWith (a future git-source inspect will need it); local inspection does no blocking work beyond file reads.

type InstallOptions added in v0.43.0

type InstallOptions struct {
	Source  string
	Ref     string
	Subpath string
}

InstallOptions parameterizes InstallWith. Source is a local directory or a git URL (required); Ref pins a branch or tag for a git source; Subpath selects a plugin directory inside the source (for monorepos shipping several plugins).

type InvokeSpec

type InvokeSpec struct {
	// Argv is the argument vector. Exactly one element must contain the
	// "{{command}}" placeholder, substituted with the full shell command line.
	Argv []string `yaml:"argv"`
	// Env are extra environment variables set on every invocation (merged over
	// the inherited process env, which wins on conflict for operator override).
	Env map[string]string `yaml:"env"`
	// TimeoutMs bounds a single invocation; 0 → DefaultRewriteTimeoutMs.
	TimeoutMs int `yaml:"timeout_ms"`
	// ApplyExitCodes are the exit codes whose stdout is taken as the rewrite.
	// Empty defaults to {0}. (rtk uses {0,3}: Default verdict maps to Ask=3.)
	ApplyExitCodes []int `yaml:"apply_exit_codes"`
	// Modes maps a generic intensity level (on|ultra) to its transform. A mode
	// absent here is still accepted (it simply applies no extra transform).
	Modes map[string]ModeSpec `yaml:"modes"`
}

InvokeSpec is the subprocess contract for a rewriter.

type LifecycleInfo added in v0.43.0

type LifecycleInfo struct {
	Index   string `json:"index,omitempty"`
	Refresh string `json:"refresh,omitempty"`
}

LifecycleInfo is the detail-facing projection of a LifecycleSpec.

type LifecycleSpec

type LifecycleSpec struct {
	Index   string `yaml:"index"`
	Refresh string `yaml:"refresh"`
}

LifecycleSpec declares index/refresh commands for graph-building plugins. Commands are shell strings run via `sh -c` with placeholders expanded.

type LocateSpec

type LocateSpec struct {
	Env   string   `yaml:"env"`
	Bin   string   `yaml:"bin"`
	Paths []string `yaml:"paths"`
}

LocateSpec resolves a binary: env override first, then PATH (Bin), then the conventional install Paths. Empty fields are skipped.

type MCPServerInfo added in v0.43.0

type MCPServerInfo struct {
	Name      string   `json:"name"`
	Transport string   `json:"transport"`
	Command   string   `json:"command,omitempty"`
	Args      []string `json:"args,omitempty"`
	URL       string   `json:"url,omitempty"`
}

MCPServerInfo is the detail-facing projection of an MCPServerSpec: the server's name, transport, and how it is reached (command+args for stdio, url for http/sse).

type MCPServerSpec

type MCPServerSpec struct {
	Name      string            `yaml:"name"`
	Transport string            `yaml:"transport"`
	Command   string            `yaml:"command"`
	Args      []string          `yaml:"args"`
	URL       string            `yaml:"url"`
	Headers   map[string]string `yaml:"headers"`
	Env       map[string]string `yaml:"env"`
}

MCPServerSpec declares an MCP server contribution. It mirrors the runtime mcp.ServerConfig shape; placeholders ({{workspace}}, {{plugin.dir}}, {{plugin.cache}}) are expanded at activation time.

type Manifest

type Manifest struct {
	// Name is the plugin's unique id (kebab-case). It is also the directory
	// name under ~/.iterion/plugins/ and the enable/disable key.
	Name string `yaml:"name"`
	// Version is free-form (semver recommended). Surfaced in `plugin list`.
	Version string `yaml:"version"`
	// Description is the one-line summary shown in listings.
	Description string `yaml:"description"`
	// Author is free-form attribution.
	Author string `yaml:"author"`
	// SchemaVersion defaults to 1 when omitted.
	SchemaVersion int `yaml:"schema_version"`
	// DefaultEnabled is the enable state when the operator has expressed no
	// preference in plugins.yaml. rtk ships true; KG explorers ship false.
	DefaultEnabled bool `yaml:"default_enabled"`
	// AutoIndex, when true, runs the lifecycle `index` command before a run if
	// the plugin is enabled and contributes a lifecycle.
	AutoIndex bool `yaml:"auto_index"`
	// Contributes lists the typed extension points this plugin provides.
	Contributes Contributes `yaml:"contributes"`
	// Config declares user-configurable settings (like a Firefox add-on's
	// preferences). The operator sets values in the studio; they are stored in
	// plugins.yaml and substituted into the manifest's mcp env/args, rewriter
	// env, and lifecycle commands via {{config.<key>}} placeholders.
	Config []ConfigField `yaml:"config"`
}

Manifest is a parsed `plugin.yaml`.

func ParseManifest

func ParseManifest(data []byte) (*Manifest, error)

ParseManifest decodes and validates a plugin.yaml document.

func SynthesizeSkillsManifest

func SynthesizeSkillsManifest(name, dir string) (*Manifest, error)

SynthesizeSkillsManifest builds a skills-only Manifest for a directory that ships bare Claude-style skills but no plugin.yaml — the common shape of a public skill library. It collects markdown skills under <dir>/skills/ (recursively); if there is no skills/ directory it falls back to top-level *.md files. The returned manifest is disabled by default (an installed third-party skill pack should be opt-in). Returns an error when no skill files are found, so the caller can report that the repo is not installable.

func (Manifest) Kinds

func (m Manifest) Kinds() []string

Kinds summarises which contribution points a manifest provides.

func (*Manifest) Validate

func (m *Manifest) Validate() error

Validate checks the manifest is well-formed and that every contribution is usable. It is intentionally strict — a malformed plugin should fail at load, not silently contribute nothing.

type MirrorKind

type MirrorKind struct {
	Name string // "skill" | "command" | "agent"
	Dir  string // ".claude/<dir>" leaf: "skills" | "commands" | "agents"
}

MirrorKind names a markdown contribution kind mirrored into a workspace .claude/<dir>/ directory.

type ModeSpec

type ModeSpec struct {
	// InjectFlag, when set, is inserted right after the binary name in the
	// produced rewrite (e.g. rtk "ultra" → insert "--ultra-compact").
	InjectFlag string `yaml:"inject_flag"`
}

ModeSpec is the per-mode transform applied to a successful rewrite.

type Plugin

type Plugin struct {
	Manifest Manifest
	// Builtin is true for plugins embedded in the binary.
	Builtin bool
	// Dir is the absolute install directory for an installed plugin; "" for a
	// builtin (its files live in the embedded FS).
	Dir string
	// Enabled is the resolved enable state (operator state || default_enabled).
	Enabled bool
	// contains filtered or unexported fields
}

Plugin is a loaded plugin: its manifest plus where it came from and how to read its bundled files (skills).

func LoadDir added in v1.0.0

func LoadDir(name, dir string) (*Plugin, error)

LoadDir reads a plugin from an arbitrary directory — a tree fetched from a git remote rather than installed under the iterion home.

It accepts both shapes an operator can host in a repository: a full plugin.yaml, or a bare skills/ library (no manifest), for which it synthesizes a skills-only manifest exactly like `iterion plugin install` does. name seeds the synthesized manifest and is ignored when the directory carries its own manifest.

Enabled is left to the caller: a PluginSource carries its own enable state, which is the authority for a git-hosted plugin (default_enabled belongs to the artifact, not to the operator's binding).

func (*Plugin) HookFragments

func (p *Plugin) HookFragments() ([]map[string]any, error)

HookFragments reads and JSON-decodes the plugin's contributed hook settings fragments. Each fragment is a settings.json shape: either {"hooks": {...}} or the bare {<Event>: [...]} map. The returned maps are the hooks map only.

func (*Plugin) MirrorFiles

func (p *Plugin) MirrorFiles(kind MirrorKind) ([]SkillFile, error)

MirrorFiles reads the plugin's contributed files for a markdown kind.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin's manifest name.

func (*Plugin) SkillFiles

func (p *Plugin) SkillFiles() ([]SkillFile, error)

SkillFiles reads the plugin's contributed skill files (back-compat shorthand).

func (*Plugin) View

func (p *Plugin) View() View

View projects a plugin to its listing form.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the loaded set of plugins (builtins + installed) with resolved enable state. It is read-mostly; SetEnabled / Remove mutate persisted state.

func Load

func Load() (*Registry, error)

Load builds a registry from the embedded builtins and the installed plugins under <iterion-home>/plugins/, applying the persisted enable state. A malformed installed plugin is skipped (logged by the caller via the returned error slice); a malformed builtin is a programming error and fails the load.

func (*Registry) ApplyConfig

func (r *Registry) ApplyConfig(name string, submitted map[string]string) error

ApplyConfig merges submitted values over a plugin's stored config and persists. Only declared fields are accepted; a secret submitted blank keeps its prior value ("leave blank to keep"). Shared by the HTTP config handler and the `iterion plugin config` CLI so both behave identically.

func (*Registry) CacheDir

func (r *Registry) CacheDir(name string) string

CacheDir returns a per-plugin cache directory under the iterion home, used to expand the {{plugin.cache}} placeholder.

func (*Registry) DetailFor added in v0.43.0

func (r *Registry) DetailFor(name string) (Detail, error)

DetailFor builds the full detail projection for one plugin. The README is read from the plugin's own file tree — the install dir for an installed plugin, the embedded FS for a builtin (builtins ship without READMEs today, which yields an empty string through the same lookup, not a skipped path).

func (*Registry) EffectiveConfig

func (r *Registry) EffectiveConfig(name string) map[string]string

EffectiveConfig returns the named plugin's config as it is actually used: the manifest field defaults overlaid with the operator's stored values. This is the map fed to {{config.<key>}} expansion (it includes secret values, so the MCP/rewriter subprocess gets the real credential).

func (*Registry) Enabled

func (r *Registry) Enabled() []*Plugin

Enabled returns the enabled plugins, sorted by name (stable chain order).

func (*Registry) EnabledRewriterSpecs

func (r *Registry) EnabledRewriterSpecs() []RewriterSpec

EnabledRewriterSpecs returns the enabled plugins' rewriter specs with their {{config.<key>}} placeholders resolved from each plugin's effective config. {{command}} and {{workspace}}/{{plugin.*}} are left untouched for the rewrite/sandbox layers. This is how operator config reaches a rewriter's invoke env/argv — rewriters run per shell command and carry resolved env, unlike the mcp/lifecycle surfaces which expand via ExpandContext at run time.

func (*Registry) EnabledRewriters

func (r *Registry) EnabledRewriters() []RewriterContribution

EnabledRewriters returns the rewriter contributions of all enabled plugins, in stable plugin-name order — this is the rewrite chain applied to commands.

func (*Registry) ExpandContextFor

func (r *Registry) ExpandContextFor(name, workspace string) ExpandContext

ExpandContextFor builds an ExpandContext for the named plugin in a workspace.

func (*Registry) Get

func (r *Registry) Get(name string) (*Plugin, bool)

Get returns the plugin with the given name.

func (*Registry) InstallDir

func (r *Registry) InstallDir(name string) string

InstallDir returns the directory an installed plugin lives in (or would).

func (*Registry) IsEnabled

func (r *Registry) IsEnabled(name string) bool

IsEnabled reports whether the named plugin is enabled.

func (*Registry) Remove

func (r *Registry) Remove(name string) error

Remove deletes an installed plugin's directory and clears its state. Builtin plugins cannot be removed (disable them instead).

func (*Registry) SetConfig

func (r *Registry) SetConfig(name string, values map[string]string) error

SetConfig persists the operator config values for a plugin (replacing any prior values) and updates the in-memory registry. Empty values are dropped so the field falls back to its manifest default.

func (*Registry) SetEnabled

func (r *Registry) SetEnabled(name string, enabled bool) error

SetEnabled persists an enable/disable decision for a plugin and updates the in-memory state. It errors if the plugin is unknown.

func (*Registry) StoredConfig

func (r *Registry) StoredConfig(name string) map[string]string

StoredConfig returns a copy of the operator-set values for a plugin (no defaults), for the config handler's merge logic.

func (*Registry) ViewFor

func (r *Registry) ViewFor(name string) (View, bool)

ViewFor returns the full listing view (incl. config schema + masked values) for one plugin. Used by the install/config handlers to echo fresh state.

func (*Registry) Views

func (r *Registry) Views() []View

Views returns the listing form of every loaded plugin, each with its config schema + current (secret-masked) values filled in.

type RewriterContribution

type RewriterContribution struct {
	Plugin string
	Spec   RewriterSpec
}

RewriterContribution pairs a rewriter spec with its owning plugin name.

type RewriterInfo added in v0.43.0

type RewriterInfo struct {
	ID           string   `json:"id"`
	SandboxMount string   `json:"sandbox_mount,omitempty"`
	TimeoutMS    int      `json:"timeout_ms,omitempty"`
	Argv         []string `json:"argv,omitempty"`
}

RewriterInfo is the detail-facing projection of a RewriterSpec.

type RewriterSpec

type RewriterSpec struct {
	// ID is the rewriter's id within the chain (usually equals the plugin name
	// for a single-rewriter plugin).
	ID string `yaml:"id"`
	// Locate resolves the binary path.
	Locate LocateSpec `yaml:"locate"`
	// Invoke describes the subprocess contract.
	Invoke InvokeSpec `yaml:"invoke"`
	// SandboxMount, when set, is the in-container path the host binary is
	// bind-mounted to for sandboxed runs (e.g. /usr/local/bin/rtk).
	SandboxMount string `yaml:"sandbox_mount"`
}

RewriterSpec declares a command-output rewriter backed by an external binary. It fully captures, declaratively, what was hardcoded for rtk: how to locate the binary, how to invoke it, which exit codes mean "apply the rewrite", and the per-mode transform of the produced rewrite.

func (*RewriterSpec) ApplyExitCodesOrDefault

func (r *RewriterSpec) ApplyExitCodesOrDefault() []int

ApplyExitCodesOrDefault returns the configured apply exit codes, defaulting to {0} when none are declared.

type SkillFile

type SkillFile struct {
	Name    string
	Content []byte
}

SkillFile is a resolved skill: its base name and content.

type View

type View struct {
	Name        string   `json:"name"`
	Version     string   `json:"version,omitempty"`
	Description string   `json:"description,omitempty"`
	Author      string   `json:"author,omitempty"`
	Enabled     bool     `json:"enabled"`
	Builtin     bool     `json:"builtin"`
	Kinds       []string `json:"kinds"`
	// ConfigSchema is the plugin's declared config fields (empty when the plugin
	// has no config block). ConfigValues carries the current values for the
	// NON-secret fields; ConfigSecretSet names the secret fields that currently
	// have a value (their value is never sent to the studio). The registry fills
	// the value-bearing fields (Plugin.View only knows the schema).
	ConfigSchema    []ConfigField     `json:"config_schema,omitempty"`
	ConfigValues    map[string]string `json:"config_values,omitempty"`
	ConfigSecretSet []string          `json:"config_secret_set,omitempty"`
}

View is the listing-facing projection of a plugin (name, enable state, and which contribution kinds it provides). Shared by the CLI and the HTTP server so both render plugins identically without an import cycle.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL