Releases: ai-plugin-marketplace/tools
Release list
@ai-plugin-marketplace/core@0.10.0
Minor Changes
-
#99
7efeba9Thanks @mike-north! - Makeaipm add-targetpreserve-or-warn instead of refusing an already-materialized target and
generating a schema-invalid skeleton on retry.Previously,
add-targetthrewRefusing to overwritewhen any file the target would write
already existed — including the common case where the target is already fully scaffolded — and
the printed remedy ("remove the file, then re-run") regenerated a placeholder with a blanked
description, which is schema-invalid for targets whose manifest requires a non-empty
description(e.g. Vercel'sSKILL.md), immediately failingaipm build.- An already-materialized target (every file it would write already exists) is now a friendly
no-op:addTarget()resolves with{ status: 'already-present', written: [], preserved: [...] }
and the CLI prints'<target>' is already present in <plugin>; nothing to do.instead of
throwing. - Existing files are never overwritten. For a multi-file target with a partial conflict, the
existing file(s) are preserved untouched and only the missing file(s) are written
(status: 'partially-added'); the CLI reports which files were preserved so the author can
review them by hand. addTarget()'s public return type changes fromPromise<void>to
Promise<AddTargetOutcome>(new exported type:{ target, status, written, preserved }).- Placeholder fields a schema requires to be non-empty (Vercel's
SKILL.mddescription) are now
emitted as non-empty placeholder prose instead of a blank string, so add-target's own output
always passesaipm build/aipm validate.
- An already-materialized target (every file it would write already exists) is now a friendly
-
#113
5e45ad5Thanks @mike-north! - Reject a Claude manifesthooksreference to the auto-loadedhooks/hooks.jsonClaude Code auto-loads
<pluginDir>/hooks/hooks.jsonand refuses to load a plugin whose
.claude-plugin/plugin.jsonhooksfield names that same file ("Duplicate hooks file detected …
The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference
additional hook files").aipm validatenow emits a hardschema-invalidfinding when thehooks
field — a string, or any string entry when it is an array — normalizes tohooks/hooks.json, with
a hint to drop the reference. Claude's generated hooks artifact remainshooks/claude.json. -
#106
45d20f7Thanks @mike-north! - Warnaipm initabout an ancestor pnpm workspace instead of leaving newcomers to silently
corrupt it, and sync the CLI'sREADME.md/docs quick start withaipm --help.A directory with no local
package.jsonthat sits under an ancestorpnpm-workspace.yamlhas
pnpm add/pnpm installsilently target the ANCESTOR's manifest and lockfile instead of a new
repo's own — a first-time user following the old quick start verbatim could corrupt an unrelated
parent project with no warning.init()'s public return type changes fromPromise<void>toPromise<InitOutcome>(new
exported type:{ ancestorWorkspace?: string }).ancestorWorkspaceis the absolute path to an
ancestorpnpm-workspace.yaml, when one exists above the newly scaffolded directory.aipm initprints a warning to stderr whenancestorWorkspaceis set, before telling the user
to runpnpm install.packages/cli/README.md("GUIDE.md" in the published package) now: requiresnpm init -y
before the install command, with an explicit workspace-hazard note; documents thelint
command and every subcommand's flags (previously undocumented, though implemented); includes
the quick start's install step; and replaces the dead../../docs/specs/architecture.md
relative link (only resolvable inside the monorepo checkout, not the published npm package)
with an absolute GitHub URL.
-
#107
9467da0Thanks @mike-north! - Removevercelfrom the default set of targets a freshaipm scaffold/core.scaffold()declares.vercel's only build artifact is an author-authoredskills/<name>/SKILL.md, which the scaffold
never seeds — so a plugin created with the default target set declaredvercelbut emitted zero
artifacts for it anywhere onaipm build, whileaipm list-targets/aipm check-supportreported
it as fully supported. A fresh scaffold now declares every known target exceptvercel;vercel
remains fully supported and can still be requested explicitly viacore.scaffold(name, { targets: [...] })or added to an existing plugin withaipm add-target <plugin> vercel.
Patch Changes
-
#98
8a72630Thanks @mike-north! - Fixaipm validate/aipm buildsilently ignoring a plugin-shapedplugins/*directory (has a
target manifest and/or a skill) that is missingaipm.config.ts. Discovery previously filtered
repo-root candidates on config presence alone, so such a directory never reached the plugin list —
validatereported green andbuildreported "Built 0 plugin(s)" with exit 0 even though the
plugin was broken and unbuildable. Discovery now also includes plugin-shaped-but-configless
directories; downstream handling is unchanged (hardenvelope-invalidfromvalidate, a thrown
error naming the missingaipm.config.tsfrombuild) — the same diagnostic a single-plugin
target missing its config already produced. A directory with neither a config nor any plugin-shape
marker is still correctly excluded from discovery. -
#110
4e5f352Thanks @mike-north! - Fix three CLI output-wording/ordering defects that misled users during a normal build/validate
run (no behavior change — the same conditions are still detected, only how/when they are
reported changes):- Pre-build freshness on a dist bundle file that was never built now reports
missingwith a
"runaipm build" hint, instead ofstale. aipm build'sBuilt N plugin(s), M artifact(s).success line no longer prints when the
post-buildvalidatestep has a hard finding that fails the run.- The
version-consistencyfinding now hints thataipm.config.tsis the source of truth for
the version, so the manifest — not the config — is the one to bump.
- Pre-build freshness on a dist bundle file that was never built now reports
-
#104
fa00aaeThanks @mike-north! - Fixaipm init'sREADME.mdandaipm scaffold's KiroPOWER.mdemitting literal
backslash-backtick sequences (\`, byte pair5c 60) instead of real backtick characters
(0x60), which rendered the generated Markdown visibly broken.Root cause: these templates are tagged with
String.raw(the project convention for multi-line
embedded Markdown, which disables escape-sequence interpretation), but their source still wrote an
escaped backtick (\`) to embed a literal backtick character — the escape is only interpreted
in an ordinary template literal, so underString.rawit stayed as the two literal characters
backslash + backtick. Both templates now interpolate abt = '`'constant instead of escaping
the backtick in the template source.Also fixes Kiro's
POWER.md"Related Files" bullet, which presentedsteering/as an existing
sibling file even though a freshly-scaffolded plugin has nosteering/directory (Kiro's scaffold
contributes onlyPOWER.md) — it now reads`steering/` (optional, hand-authored) — add Kiro steering files here if needed. -
#100
dbc0e68Thanks @mike-north! - Fixaipm validateandaipm lintsilently accepting a Claudeplugin.jsonthat is valid JSON but omits a schema-required field (e.g.name) — this now emits a hardschema-invalidfinding /schema/target-conformancediagnostic, matching the other targets' behavior.aipm lint --format json'ssummary.fileCountnow reflects the files a run actually scanned (LintResult.scannedFiles) instead of the number of distinct files a diagnostic happened to be attached to, which previously stayed pinned regardless of manifest changes.
@ai-plugin-marketplace/cli@0.7.0
Minor Changes
-
#99
7efeba9Thanks @mike-north! - Makeaipm add-targetpreserve-or-warn instead of refusing an already-materialized target and
generating a schema-invalid skeleton on retry.Previously,
add-targetthrewRefusing to overwritewhen any file the target would write
already existed — including the common case where the target is already fully scaffolded — and
the printed remedy ("remove the file, then re-run") regenerated a placeholder with a blanked
description, which is schema-invalid for targets whose manifest requires a non-empty
description(e.g. Vercel'sSKILL.md), immediately failingaipm build.- An already-materialized target (every file it would write already exists) is now a friendly
no-op:addTarget()resolves with{ status: 'already-present', written: [], preserved: [...] }
and the CLI prints'<target>' is already present in <plugin>; nothing to do.instead of
throwing. - Existing files are never overwritten. For a multi-file target with a partial conflict, the
existing file(s) are preserved untouched and only the missing file(s) are written
(status: 'partially-added'); the CLI reports which files were preserved so the author can
review them by hand. addTarget()'s public return type changes fromPromise<void>to
Promise<AddTargetOutcome>(new exported type:{ target, status, written, preserved }).- Placeholder fields a schema requires to be non-empty (Vercel's
SKILL.mddescription) are now
emitted as non-empty placeholder prose instead of a blank string, so add-target's own output
always passesaipm build/aipm validate.
- An already-materialized target (every file it would write already exists) is now a friendly
-
#106
45d20f7Thanks @mike-north! - Warnaipm initabout an ancestor pnpm workspace instead of leaving newcomers to silently
corrupt it, and sync the CLI'sREADME.md/docs quick start withaipm --help.A directory with no local
package.jsonthat sits under an ancestorpnpm-workspace.yamlhas
pnpm add/pnpm installsilently target the ANCESTOR's manifest and lockfile instead of a new
repo's own — a first-time user following the old quick start verbatim could corrupt an unrelated
parent project with no warning.init()'s public return type changes fromPromise<void>toPromise<InitOutcome>(new
exported type:{ ancestorWorkspace?: string }).ancestorWorkspaceis the absolute path to an
ancestorpnpm-workspace.yaml, when one exists above the newly scaffolded directory.aipm initprints a warning to stderr whenancestorWorkspaceis set, before telling the user
to runpnpm install.packages/cli/README.md("GUIDE.md" in the published package) now: requiresnpm init -y
before the install command, with an explicit workspace-hazard note; documents thelint
command and every subcommand's flags (previously undocumented, though implemented); includes
the quick start's install step; and replaces the dead../../docs/specs/architecture.md
relative link (only resolvable inside the monorepo checkout, not the published npm package)
with an absolute GitHub URL.
-
#107
9467da0Thanks @mike-north! - Removevercelfrom the default set of targets a freshaipm scaffold/core.scaffold()declares.vercel's only build artifact is an author-authoredskills/<name>/SKILL.md, which the scaffold
never seeds — so a plugin created with the default target set declaredvercelbut emitted zero
artifacts for it anywhere onaipm build, whileaipm list-targets/aipm check-supportreported
it as fully supported. A fresh scaffold now declares every known target exceptvercel;vercel
remains fully supported and can still be requested explicitly viacore.scaffold(name, { targets: [...] })or added to an existing plugin withaipm add-target <plugin> vercel.
Patch Changes
-
#98
8a72630Thanks @mike-north! - Fixaipm validate/aipm buildsilently ignoring a plugin-shapedplugins/*directory (has a
target manifest and/or a skill) that is missingaipm.config.ts. Discovery previously filtered
repo-root candidates on config presence alone, so such a directory never reached the plugin list —
validatereported green andbuildreported "Built 0 plugin(s)" with exit 0 even though the
plugin was broken and unbuildable. Discovery now also includes plugin-shaped-but-configless
directories; downstream handling is unchanged (hardenvelope-invalidfromvalidate, a thrown
error naming the missingaipm.config.tsfrombuild) — the same diagnostic a single-plugin
target missing its config already produced. A directory with neither a config nor any plugin-shape
marker is still correctly excluded from discovery. -
#110
4e5f352Thanks @mike-north! - Fix three CLI output-wording/ordering defects that misled users during a normal build/validate
run (no behavior change — the same conditions are still detected, only how/when they are
reported changes):- Pre-build freshness on a dist bundle file that was never built now reports
missingwith a
"runaipm build" hint, instead ofstale. aipm build'sBuilt N plugin(s), M artifact(s).success line no longer prints when the
post-buildvalidatestep has a hard finding that fails the run.- The
version-consistencyfinding now hints thataipm.config.tsis the source of truth for
the version, so the manifest — not the config — is the one to bump.
- Pre-build freshness on a dist bundle file that was never built now reports
-
#104
fa00aaeThanks @mike-north! - Fixaipm init'sREADME.mdandaipm scaffold's KiroPOWER.mdemitting literal
backslash-backtick sequences (\`, byte pair5c 60) instead of real backtick characters
(0x60), which rendered the generated Markdown visibly broken.Root cause: these templates are tagged with
String.raw(the project convention for multi-line
embedded Markdown, which disables escape-sequence interpretation), but their source still wrote an
escaped backtick (\`) to embed a literal backtick character — the escape is only interpreted
in an ordinary template literal, so underString.rawit stayed as the two literal characters
backslash + backtick. Both templates now interpolate abt = '`'constant instead of escaping
the backtick in the template source.Also fixes Kiro's
POWER.md"Related Files" bullet, which presentedsteering/as an existing
sibling file even though a freshly-scaffolded plugin has nosteering/directory (Kiro's scaffold
contributes onlyPOWER.md) — it now reads`steering/` (optional, hand-authored) — add Kiro steering files here if needed. -
#100
dbc0e68Thanks @mike-north! - Fixaipm validateandaipm lintsilently accepting a Claudeplugin.jsonthat is valid JSON but omits a schema-required field (e.g.name) — this now emits a hardschema-invalidfinding /schema/target-conformancediagnostic, matching the other targets' behavior.aipm lint --format json'ssummary.fileCountnow reflects the files a run actually scanned (LintResult.scannedFiles) instead of the number of distinct files a diagnostic happened to be attached to, which previously stayed pinned regardless of manifest changes. -
#103
43db451Thanks @mike-north! - Fix-h/--helpon subcommands (build,validate,lint,scaffold,init,migrate,check-support,add-target,list-targets) executing the subcommand instead of printing usage —aipm build --helpran a real build, andaipm validate --helpmisparsed--helpas the target path.--help/-hnow short-circuits to usage and exits 0 before any argument parsing or side effect, for every subcommand. -
Updated dependencies [
7efeba9,5e45ad5,8a72630,4e5f352,fa00aae,dbc0e68,45d20f7, [9467da0](https://gi...
@ai-plugin-marketplace/core@0.9.0
Minor Changes
-
#81
cb16c38Thanks @mike-north! - Guardaipm buildagainst a stale installed toolkit silently reverting generated artifacts.Every sentinel-carrying generated artifact is now stamped with the
@ai-plugin-marketplace/core
version that produced it (_generated.versionin JSON outputs, a# version:line in
inline/sidecar outputs). Before writing anything,aipm buildcompares the installed core version
against the version stamped into existing committed artifacts: if the installed toolkit is older
(by semver precedence), the build refuses with a non-zero exit and a message naming both versions
and suggestingpnpm install. This closes the failure mode where a checkout with a stale
node_modulesregenerates committed outputs with an older generator and silently reverts a shipped
fix. Equal-or-newer installs, first-time/unstamped trees, and same-version rebuilds proceed as
before and (re)stamp with the installed version.- New
BuildOptions.forceDowngradeand theaipm build --force-downgradeflag override the guard. - The freshness check ignores the version stamp, so a version bump alone no longer marks committed
artifacts stale.
- New
-
#84
6d2ee20Thanks @mike-north! -aipm validate(andlint()'scorrectness/version-consistencyrule) now fails when a declared
target's manifestversionfield (.claude-plugin/plugin.json,.codex-plugin/plugin.json,
.cursor-plugin/plugin.json,gemini-extension.json,POWER.mdfrontmatter,
.plugin/plugin.json) does not matchaipm.config.ts'sversion. Installs are keyed by manifest
version, so a stale author-maintained manifest previously let a release ship withaipm.config.ts
bumped but the manifest still pointing at the old version — auto-update silently kept serving the
pre-release artifact. Mirrors the existingname-consistencycheck: a newversion-consistency
FindingCode, hard severity, one finding per mismatched manifest.Fixes a related scaffold bug this check surfaced:
aipm scaffoldwroteaipm.config.tswith
version: '0.1.0'while every per-target scaffolded manifest wroteversion: '0.0.1', so a
freshly-scaffolded plugin failedversion-consistencyimmediately.aipm scaffoldnow emits
'0.0.1'consistently everywhere.
Patch Changes
-
#78
d0ac824Thanks @mike-north! - Fix the emitted Cursor controller-shim runner (hooks/cursor-shim.mjs) dropping a handler's
denial message when it gates via the top-levelcontinue: falseshape. Per the Claude Code hooks
contract, acontinue: falsedenial carries its user-facing message instopReason—reasonis
scoped to the separatedecision: "block"shape. The runner'sinterpretPreToolUseand
interpretUserPromptSubmitonly ever readparsed.reason, so a handler following the documented
continue/stopReasoncontract still denied correctly but Cursor showed the user a bare,
unexplained denial.Both interpreters now fall back
reason→stopReasonwhen translating acontinue: false
denial to Cursor's flat control JSON (agent_messageforpreToolUse,user_messagefor
beforeSubmitPrompt), preferringreasonif a handler sets both. The existing
decision: "block"(withreason) behavior is unchanged, and acontinue: falsepayload with
neither field still denies without a message and without crashing. (#57) -
#79
f061725Thanks @mike-north! -hooks/payload-adapteris no longer emitted for a plugin whosehooks/claude.yamldeclares zero hook events (hooks: {}) — previously the 83-line adapter script (and its sentinel) were emitted even when nothing in the plugin could ever invoke them. The emitted--schemaoutput is now pre-sorted at generation time instead of spawningjq -S .on every invocation (byte-identical output, no runtime cost), the generated banners inhooks/payload-adapterandhooks/cursor-shim.mjsnow qualify theirdocs/specs/*.mdpointers with@ai-plugin-marketplace/toolsso they resolve for a reader in a consumer repo, andhooks/payload-adapter's temp-filemktempcall now uses an explicit template.
@ai-plugin-marketplace/cli@0.6.0
Minor Changes
-
#81
cb16c38Thanks @mike-north! - Guardaipm buildagainst a stale installed toolkit silently reverting generated artifacts.Every sentinel-carrying generated artifact is now stamped with the
@ai-plugin-marketplace/core
version that produced it (_generated.versionin JSON outputs, a# version:line in
inline/sidecar outputs). Before writing anything,aipm buildcompares the installed core version
against the version stamped into existing committed artifacts: if the installed toolkit is older
(by semver precedence), the build refuses with a non-zero exit and a message naming both versions
and suggestingpnpm install. This closes the failure mode where a checkout with a stale
node_modulesregenerates committed outputs with an older generator and silently reverts a shipped
fix. Equal-or-newer installs, first-time/unstamped trees, and same-version rebuilds proceed as
before and (re)stamp with the installed version.- New
BuildOptions.forceDowngradeand theaipm build --force-downgradeflag override the guard. - The freshness check ignores the version stamp, so a version bump alone no longer marks committed
artifacts stale.
- New
-
#84
6d2ee20Thanks @mike-north! -aipm validate(andlint()'scorrectness/version-consistencyrule) now fails when a declared
target's manifestversionfield (.claude-plugin/plugin.json,.codex-plugin/plugin.json,
.cursor-plugin/plugin.json,gemini-extension.json,POWER.mdfrontmatter,
.plugin/plugin.json) does not matchaipm.config.ts'sversion. Installs are keyed by manifest
version, so a stale author-maintained manifest previously let a release ship withaipm.config.ts
bumped but the manifest still pointing at the old version — auto-update silently kept serving the
pre-release artifact. Mirrors the existingname-consistencycheck: a newversion-consistency
FindingCode, hard severity, one finding per mismatched manifest.Fixes a related scaffold bug this check surfaced:
aipm scaffoldwroteaipm.config.tswith
version: '0.1.0'while every per-target scaffolded manifest wroteversion: '0.0.1', so a
freshly-scaffolded plugin failedversion-consistencyimmediately.aipm scaffoldnow emits
'0.0.1'consistently everywhere.
Patch Changes
@ai-plugin-marketplace/core@0.8.0
Minor Changes
-
#72
6100387Thanks @mike-north! - Addaipm lint [path] [--as <mode>] [--format text|json|sarif] [--rule <id>=<severity> ...], exposing the lint engine core (#61) via the CLI with machine-readable output.text(default): grouped by file asfile:line:col ruleId severity message;--verboseappends the docs URL; diagnostics without arangerender asfile ruleId severity message(position segment omitted, never zero-filled).json: the rawDiagnostic[]plus a summary envelope (errorCount/warnCount/infoCount/fileCount).sarif: SARIF 2.1.0, onerules[]entry per distinct rule id — validated in tests against the official SARIF 2.1.0 JSON Schema.- Exit codes:
0noerror-severity diagnostics,1errors present,2usage error (unknown--format, malformed--rule, or an unsupported--asmode — onlyaipm-repois implemented; foreign discovery modes are a later issue). --rule <id>=<severity>(repeatable) overrides a rule's severity post-hoc, or drops its diagnostics entirely with=off.- New core export:
applyRuleSeverityOverrides(diagnostics, overrides), the pure filter backing--rule. aipm validatebehavior and exit codes are unchanged.
-
#70
2b34d54Thanks @mike-north! - Add the position-aware lint engine core (Diagnostic/Rule/document-layer types,lint()) and migrate every existingvalidate()check onto it, withaipm validatebehavior unchanged.- New
packages/core/src/lint/module:Diagnostic,Range,Rule,RuleContexttypes (L-D1/L-D4), a position-aware document layer for JSON (jsonc-parser), YAML (theyamlpackage's CST), and markdown frontmatter, and a purediagnosticToFinding()mapping back to the legacyFindingshape. - Every existing validate check (envelope shape, per-target schema, envelope adherence, frontmatter parsing, name consistency, MCP key sync, marketplace registration, freshness, default-marketplace-name) is now backed by a
Ruleobject carrying its legacyFindingCode. - Four new
correctness/*rules:broken-file-ref,unknown-hook-event,invalid-matcher,duplicate-component-name. - New public exports:
lint(path, options): Promise<LintResult>, plusDiagnostic,Range,Rule,RuleContext,Document/JsonDocument/YamlDocument/FrontmatterDocument,Fix,Position,LintOptions,LintResult, andConfigCache. jsonc-parseris now a direct dependency (previously transitive only).
- New
Patch Changes
-
#60
d1e1af3Thanks @mike-north! - Anchor the Cursor controller-shim invocation to${CLAUDE_PLUGIN_ROOT:-.}instead of a cwd-relative
path. The 0.7.0 transform emitted gating hook commands asnode ./hooks/cursor-shim.mjs …, which
assumed Cursor runs plugin hook commands with cwd = plugin root — an assumption Cursor does not
guarantee. Because shimmed entries setfailClosed: true, a shim path that failed to resolve did
not merely disable the gate:nodeexited "Cannot find module" and every gated tool call was
denied for that plugin under Cursor.The emitted command is now
node "${CLAUDE_PLUGIN_ROOT:-.}/hooks/cursor-shim.mjs" <event> -- '<handler>'(double-quoted, so a
plugin root containing spaces survives shell expansion). The:-.fallback matters because Cursor's
behavior differs by consumption layout: for an installed plugin,${CLAUDE_PLUGIN_ROOT}is set
to the plugin's install path (confirmed only by Cursor staff forum posts, not the official docs), so
the invocation anchors there regardless of cwd; for project-level/colocated hooks (a project's
own.cursor/hooks.json), Cursor does not set the variable at all — verified empirically against a
realcursor-agentbuild — so an unconditional${CLAUDE_PLUGIN_ROOT}anchor would expand to an
empty string and, combined withfailClosed: true, deny every gated call. The.fallback resolves
relative to cwd (project root for project-level hooks), preserving the previously-working colocated
behavior.The enforcement UAT now covers both layouts explicitly: a project-level/colocated scenario with
CLAUDE_PLUGIN_ROOTdeleted from the spawn environment, and an installed-plugin scenario with the
hook cwd and the shim directory deliberately different andCLAUDE_PLUGIN_ROOTset — the case the
original workspace-colocated UAT could not detect. (#56)
@ai-plugin-marketplace/cli@0.5.0
Minor Changes
- #72
6100387Thanks @mike-north! - Addaipm lint [path] [--as <mode>] [--format text|json|sarif] [--rule <id>=<severity> ...], exposing the lint engine core (#61) via the CLI with machine-readable output.text(default): grouped by file asfile:line:col ruleId severity message;--verboseappends the docs URL; diagnostics without arangerender asfile ruleId severity message(position segment omitted, never zero-filled).json: the rawDiagnostic[]plus a summary envelope (errorCount/warnCount/infoCount/fileCount).sarif: SARIF 2.1.0, onerules[]entry per distinct rule id — validated in tests against the official SARIF 2.1.0 JSON Schema.- Exit codes:
0noerror-severity diagnostics,1errors present,2usage error (unknown--format, malformed--rule, or an unsupported--asmode — onlyaipm-repois implemented; foreign discovery modes are a later issue). --rule <id>=<severity>(repeatable) overrides a rule's severity post-hoc, or drops its diagnostics entirely with=off.- New core export:
applyRuleSeverityOverrides(diagnostics, overrides), the pure filter backing--rule. aipm validatebehavior and exit codes are unchanged.
Patch Changes
@ai-plugin-marketplace/core@0.7.0
Minor Changes
-
#40
8251430Thanks @mike-north! - Contract-translate Cursor controller hooks with a generated fail-closed shim. The shipped Cursor
hooks transform is observer-only: a Claude-authored block/deny gate emitted through it fails OPEN on
Cursor, because the two harnesses' handler contracts diverge on tool identity (ShellvsBash),
event casing, control-output shape, and failure default.aipm buildnow translates that contract
for gating events so controller hooks enforce correctly.Classification is static, by event: a committed
GATING_EVENTSset (PreToolUse,
UserPromptSubmit) is treated as controllers;PostToolUse/Stopfire after the decision point,
cannot block, and stay on the byte-identical observer path. For a gating event, each generated
hooks/cursor.jsonentry'scommandbecomesnode ./hooks/cursor-shim.mjs <cursorEvent> -- <original handler command>withfailClosed: true(the handler keeps its own args verbatim after
the--boundary).When a plugin has at least one gating-event hook, the build additionally emits a static Node runner
hooks/cursor-shim.mjsplus itshooks/cursor-shim.mjs.generatedsidecar sentinel (the runner is
pure executable JS, so the sentinel lives in the companion file). The runner reads Cursor's hook
stdin, translates it to a Claude envelope (Shell→Bash, PascalCase event,session_id/
tool_input.commandpass-through), spawns the handler, and translates the handler's Claude control
output back to Cursor's flat control JSON (permissionDecision→permission;decision:"block"+
reason →permission:"deny"+agent_message;beforeSubmitPromptblock →
{ continue: false, user_message }). It is fail-closed: a non-zero handler exit, malformed handler
output, bad arguments, or a spawn error emit a deny and exit 2, always as valid JSON. The
cursorHooksFileSchemanow acceptsfailClosedon an entry, and all three artifacts are
freshness-checked byte-for-byte. -
#54
600e3a0Thanks @mike-north! -GeneratedFile.targetnow accepts'shared'in addition to aTargetId(the new
GeneratedFileTarget = TargetId | 'shared'type). Two build artifacts genuinely have no single
owning target —hooks/payload-adapter(and its sidecar), emitted for any plugin authoring hooks
regardless of which targets it declares, and the generated-root sidecar manifest
(.aipm/generated-root.json), which spans every emitted single-artifact-host/registry owner — and
were previously attributed to an arbitrary, deterministically-chosen single target as a workaround.
Both now reporttarget: 'shared'instead.Consumers reading
BuildResult.artifacts[].targetand narrowing onTargetIdshould account for
the new'shared'value; aswitchoverTargetIdalone will no longer be exhaustive against
GeneratedFileTarget. -
#49
feabd3bThanks @mike-north! - Emit a generated cross-harness hook payload adapter (hooks/payload-adapter) for every plugin
that authorshooks/claude.yaml.hooks/claude.yamlis authored once in the Claude Code dialect;
Codex is near-identical but not quite (tool_responsevstool_output, extra additive fields), so
plugin hook handler code previously had to re-derive those deltas by hand. The adapter is a static
sh+jqfilter, emitted regardless of which target(s) the plugin's envelope declares, that
normalizes any supported harness's raw hook stdin payload into one documented canonical shape (the
Claude Code hook envelope, additively extended) — seedocs/specs/payload-adapter.md.Behavior: the canonical shape is the Claude Code hook envelope; Codex's
tool_responsegains a
canonicaltool_outputalongside it (never removing the original field); aharness: {name}
envelope is added, detected from Codex's additive-only fields (turn_id/model/tool_response/
agent_transcript_path), then theCODEX_HOMEenvironment variable as a secondary signal, then a
recognized PascalCasehook_event_name, else"unknown";is_subagentis added for every payload
from a non-emptyagent_id;payload-adapter --schemaprints the canonical JSON Schema plus a
single-sourced contract version and exits 0 without reading stdin; a missingjqonPATH
degrades to a byte-for-byte stdin passthrough, exit 0 (never breaks a hook chain); output key order
is sorted at every nesting level for deterministic, golden-able output.The adapter is a byte-exact static asset (like the existing Cursor controller-hook shim) — every
plugin that authors hooks receives identical bytes, andhooks/payload-adapter.generatedcarries
its sidecar sentinel so freshness compares it byte-for-byte alongside the existing generated hook
artifacts.
Patch Changes
-
#43
9937b99Thanks @mike-north! - Harden the generated Cursor controller-hook shim (hooks/cursor-shim.mjs).- Shell fidelity. The transform now embeds the original Claude handler command as a single
POSIX-single-quoted token after the--sentinel, and the runner executes everything after--
through a shell (spawnSync(cmd, { shell: true, … })) — matching Claude's ownsh -chook
model. A handler command using env-var refs, quoting, or its own args now execs correctly on
Cursor instead of failing to run (and denying). - No stdout truncation. The runner flushes stdout before exiting
(process.stdout.write(json, () => process.exit(code))) on the fail-closed, allow/continue, and
interpret paths, so a large allow decision is never truncated into malformed JSON. - Explicit spawn
maxBuffer(64 MB). A handler emitting more than the default 1 MB of stdout is
no longer misread as a spawn failure and denied. - Single YAML parse for Cursor. "Has a gating hook?" is derived from the already-converted
Cursor document rather than a second parse of the source. - Single source of truth for the tool table. The runner's
CURSOR_TO_CLAUDE_TOOLSis generated
from the exported const at emit time (stable, sorted key order — the.mjsstays
byte-deterministic), so the two copies cannot drift.
Fail-closed safety is unchanged: a non-zero handler exit, malformed handler output, bad argv, spawn
failure, or internal error still emits a deny and exits 2, always as valid JSON. - Shell fidelity. The transform now embeds the original Claude handler command as a single
@ai-plugin-marketplace/cli@0.4.1
@ai-plugin-marketplace/core@0.6.0
Minor Changes
-
#35
0f4eeceThanks @mike-north! - Add a Cursor hooks build target. When a plugin's envelope includescursorand it ships a
hooks/claude.yamlsource,aipm buildnow emits a Cursor-formathooks/cursor.jsonderived
mechanically from that source — parallel to the existingclaude→hooks/claude.jsonand
gemini→hooks/hooks.jsonfan-out.The transform renames Claude events to Cursor's camelCase vocabulary (
PreToolUse→preToolUse,
PostToolUse→postToolUse,Stop→stop,UserPromptSubmit→beforeSubmitPrompt),
translates matcher tool names (Bash→Shell;Read/Write/Edit/Grepidentity; unmapped
matchers pass through), and reshapes Claude's nested matcher blocks into Cursor's flat
{ command, type?, matcher? }entries under a{ version: 1, hooks: … }envelope. Source events
with no Cursor equivalent are dropped. The generated file carries the standard_generated
sentinel and is freshness-checked like the other hook JSONs.aipm validatenow validates a presenthooks/cursor.jsonagainst a strict schema (HARD
schema-invalidon failure), and the Cursor manifest guidance points thehooksfield at
./hooks/cursor.jsoninstead of the Claude-formathooks/claude.json. -
#30
a560f7cThanks @mike-north! - Add soft Open Plugins conformance advisories on the nativeclaude/cursor/codextargets.aipm validatenow surfaces a new softopen-plugins-conformancefinding (added to the
FindingCodeunion) that nudges native plugins toward Open Plugins portability without ever failing
them — it never flipsValidationResult.passed. Two advisories fire today:- Name-grammar drift — a plugin
namethat is valid for the native target but violates the Open
Plugins name grammar (e.g.a--bor a trailing hyphen, which the native scaffold-slug regex
accepts but Open Plugins rejects). It fires only for otherwise-native-valid manifests, so a broken
name still gets its usual hard finding without a duplicate advisory. - Metadata-dir isolation — a non-
plugin.jsonentry in a plugin's vendor metadata directory
(.claude-plugin//.cursor-plugin//.codex-plugin/), which Open Plugins requires to hold
onlyplugin.json.
Also hardens path-traversal rejection: a
..segment in themcpServers(and Codexapps/hooks)
config-path fields — previously unchecked because those paths are not existence-validated — is now a
hardschema-invalidacross all three targets, matching the existing rejection on component paths.The Open Plugins
namegrammar is now a single shared source of truth
(targets/open-plugins-conformance.ts) consumed by both theopen-pluginstarget schema (where a
violation is hard) and these advisories (where it is soft). - Name-grammar drift — a plugin
-
#28
38e53a7Thanks @mike-north! - Add Open Plugins as a 7th host target (open-plugins).Open Plugins (open-plugins.com, v1.0.0) is a vendor-neutral external
standard for the on-disk shape of an AI-assistant plugin. Declaring'open-plugins'in a plugin's
envelope now emits an Open-Plugins-conformant.plugin/plugin.jsonmanifest and a repo-root
marketplace.jsonregistry (the 4th generated registry, at Open Plugins lookup position 1),
projected from the same authored source that feeds every other target.The target validates the manifest against the Open Plugins name grammar and component-path rules
(each path must be./-relative with no..), checks that declared component paths resolve on
disk, and enforces metadata-directory isolation via a new hardmetadata-dir-isolationfinding
(the.plugin/directory must contain onlyplugin.json). Adds'open-plugins'to theTargetId
union and'metadata-dir-isolation'to theFindingCodeunion (both additive).The repo-root
marketplace.jsonis protected by the generated-root collision guard: a pre-existing
marketplace.jsonthe toolkit did not generate raises a hardroot-artifact-collisionand is never
overwritten or orphan-removed.
@ai-plugin-marketplace/cli@0.4.0
Minor Changes
- #28
38e53a7Thanks @mike-north! - Recognize the newopen-pluginstarget across the CLI.aipm list-targetsnow lists
open-plugins,aipm scaffoldincludes it in the default envelope (emitting.plugin/plugin.json
and a repo-rootmarketplace.json), andaipm add-target <plugin> open-plugins,aipm build, and
aipm validatehandle it like any other host target.