Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent-context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Plugin manifests, assets, READMEs, and Cursor rules remain owned by their target

## Local development

Requires Node.js 22 or newer and has no package dependencies.
Requires Node.js 22 or newer. The only dependency is `ajv`, used to validate generated Agent Plugins artifacts.

```bash
npm ci
Expand All @@ -42,7 +42,7 @@ git -C ../../codex-plugin diff

The sync command replaces `skills/mintlify/`, writes the client-specific MCP configuration file, and writes `.mintlify-agent-context.json` with the source commit. For Kiro, it also writes the required `plugin.json`. It does not change any other plugin files.

Treat the Kiro manifest version as a release version. Whenever a change modifies the generated Kiro skill, MCP configuration, or manifest, increment `pluginManifest.version` in `targets/kiro.json` according to Semantic Versioning before merging. Do not use a Git SHA or SemVer build metadata as the update version because build metadata does not affect version precedence.
Kiro uses the manifest version to detect updates. The sync command bumps the patch version automatically whenever the generated Kiro skill, MCP configuration, or manifest differs from the target repository. For a minor or major release, set a higher `pluginManifest.version` in `targets/kiro.json`; the sync uses it when it is greater than the target's current version. Versions must be `MAJOR.MINOR.PATCH`, without pre-release tags or build metadata.

`npm run status` compares locally checked-out sibling plugin repositories with fresh builds and reports whether each one is current. Pass a workspace root as the final argument if the repositories do not share this repository's parent directory.

Expand Down
93 changes: 92 additions & 1 deletion agent-context/scripts/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,99 @@ export async function buildAll({ outputRoot, selectedIds = [] } = {}) {
return Promise.all(targets.map((target) => buildTarget(target, resolvedOutput)));
}

function parseVersion(version) {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
if (match === null) {
throw new Error(`Plugin manifest version must be MAJOR.MINOR.PATCH: ${version}`);
}
return match.slice(1).map(Number);
}

function compareVersions(a, b) {
const [left, right] = [parseVersion(a), parseVersion(b)];
for (let index = 0; index < 3; index += 1) {
if (left[index] !== right[index]) {
return left[index] - right[index];
}
}
return 0;
}

async function readTree(root, prefix = '') {
const files = new Map();
let entries;
try {
entries = await readdir(path.join(root, prefix), { withFileTypes: true });
} catch {
return files;
}
for (const entry of entries) {
const relativePath = path.join(prefix, entry.name);
if (entry.isDirectory()) {
for (const [file, contents] of await readTree(root, relativePath)) {
files.set(file, contents);
}
} else {
files.set(relativePath, await readFile(path.join(root, relativePath), 'utf8'));
}
}
return files;
}

async function readOptional(file) {
try {
return await readFile(file, 'utf8');
} catch {
return undefined;
}
}

// Snapshot of everything a manifest version describes, excluding the version itself.
async function releaseSnapshot(root, target) {
const skill = await readTree(path.join(root, 'skills', 'mintlify'));
const manifest = await readOptional(path.join(root, 'plugin.json'));
const { version, ...manifestWithoutVersion } = manifest === undefined ? {} : JSON.parse(manifest);
return JSON.stringify({
skill: [...skill].sort(([a], [b]) => a.localeCompare(b)),
mcp: await readOptional(path.join(root, target.mcpConfigFile)),
manifest: manifestWithoutVersion,
});
}

// Kiro uses the manifest version to detect updates. Bump the patch version whenever the
// released content changes; a higher version set in the target configuration wins.
export async function resolveManifestVersion(target, sourceRoot, destination) {
const configuredVersion = target.pluginManifest.version;
const existingManifest = await readOptional(path.join(destination, 'plugin.json'));
if (existingManifest === undefined) {
return configuredVersion;
}

const existingVersion = JSON.parse(existingManifest).version;
if (compareVersions(configuredVersion, existingVersion) > 0) {
return configuredVersion;
}

const changed =
(await releaseSnapshot(sourceRoot, target)) !== (await releaseSnapshot(destination, target));
if (!changed) {
return existingVersion;
}
const [major, minor, patch] = parseVersion(existingVersion);
return `${major}.${minor}.${patch + 1}`;
}

export async function copyTargetToRepository(targetId, destination, outputRoot) {
const [target] = await loadTargets([targetId]);
const sourceRoot = path.join(outputRoot, targetId);
const sourceSkill = path.join(sourceRoot, 'skills', 'mintlify');
const destinationSkill = path.join(destination, 'skills', 'mintlify');

await stat(sourceSkill);
const manifestVersion =
target.pluginManifest === undefined
? undefined
: await resolveManifestVersion(target, sourceRoot, destination);
await rm(destinationSkill, { recursive: true, force: true });
await mkdir(path.dirname(destinationSkill), { recursive: true });
await cp(sourceSkill, destinationSkill, { recursive: true });
Expand All @@ -219,7 +305,12 @@ export async function copyTargetToRepository(targetId, destination, outputRoot)
path.join(destination, target.mcpConfigFile),
);
if (target.pluginManifest !== undefined) {
await cp(path.join(sourceRoot, 'plugin.json'), path.join(destination, 'plugin.json'));
const manifest = { ...target.pluginManifest, version: manifestVersion };
validateAgentPluginArtifact('plugin', manifest, target.id);
await writeFile(
path.join(destination, 'plugin.json'),
`${JSON.stringify(manifest, null, 2)}\n`,
);
}
await cp(
path.join(sourceRoot, '.mintlify-agent-context.json'),
Expand Down
2 changes: 1 addition & 1 deletion agent-context/targets/kiro.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"pluginManifest": {
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "mintlify",
"version": "1.0.0",
"version": "1.0.1",
"description": "Create, maintain, and improve Mintlify documentation with product guidance and Mintlify tools.",
"author": {
"name": "Mintlify",
Expand Down
31 changes: 31 additions & 0 deletions agent-context/test/build.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,34 @@ test('sync replaces only generated context paths', async () => {
await rm(root, { recursive: true, force: true });
}
});

test('sync bumps the Kiro manifest patch version only when released content changes', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'mintlify-agent-context-version-test-'));
const outputRoot = path.join(root, 'dist');
const destination = path.join(root, 'kiro-power');
const readVersion = async () =>
JSON.parse(await readFile(path.join(destination, 'plugin.json'), 'utf8')).version;

try {
const [kiro] = await loadTargets(['kiro']);
await buildAll({ outputRoot, selectedIds: ['kiro'] });

await copyTargetToRepository('kiro', destination, outputRoot);
assert.equal(await readVersion(), kiro.pluginManifest.version);

await copyTargetToRepository('kiro', destination, outputRoot);
assert.equal(await readVersion(), kiro.pluginManifest.version);

const manifestPath = path.join(destination, 'plugin.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
await writeFile(manifestPath, JSON.stringify({ ...manifest, version: '1.4.2' }));
await writeFile(path.join(destination, 'skills', 'mintlify', 'SKILL.md'), 'outdated\n');
await copyTargetToRepository('kiro', destination, outputRoot);
assert.equal(await readVersion(), '1.4.3');

await copyTargetToRepository('kiro', destination, outputRoot);
assert.equal(await readVersion(), '1.4.3');
} finally {
await rm(root, { recursive: true, force: true });
}
});
Loading