Files
Rohit Ghumare 37ea1b99ad feat: cursor marketplace plugin with hooks, mcp, and skills (#1213)
* feat: cursor marketplace plugin with native hooks and mcp config

* fix: cursor payload compat and transcript prompt backfill in hooks

* fix: plugin-root hook paths, backfill ordering, session-id fallbacks

* docs: cursor plugin rows in readme, translations, and changelog

* fix: cursor native-plugin card, broken agent logos

* chore: sync openclaw and hermes plugin manifest versions

* docs: openclaw hook permission and hermes tool count

* chore: clawhub compat metadata for openclaw plugin

* docs: tested openclaw and hermes install rows in readme
2026-08-16 13:30:40 +01:00

100 lines
3.1 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
const manifest = JSON.parse(readFileSync(".cursor-plugin/plugin.json", "utf-8"));
const hooks = JSON.parse(readFileSync("plugin/cursor/hooks.json", "utf-8"));
const mcp = JSON.parse(readFileSync("plugin/cursor/mcp.json", "utf-8"));
const CURSOR_HOOK_EVENTS = new Set([
"sessionStart",
"sessionEnd",
"preToolUse",
"postToolUse",
"postToolUseFailure",
"subagentStart",
"subagentStop",
"beforeShellExecution",
"afterShellExecution",
"beforeMCPExecution",
"afterMCPExecution",
"beforeReadFile",
"afterFileEdit",
"beforeSubmitPrompt",
"preCompact",
"stop",
"afterAgentResponse",
"afterAgentThought",
"beforeTabFileRead",
"afterTabFileEdit",
"workspaceOpen",
]);
describe("Cursor plugin manifest", () => {
it("has a kebab-case name and version matching the package", () => {
expect(manifest.name).toMatch(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/);
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
expect(manifest.version).toBe(pkg.version);
});
it("references only paths that exist in the repo", () => {
for (const p of [
manifest.skills,
manifest.hooks,
manifest.mcpServers,
manifest.logo,
]) {
expect(typeof p).toBe("string");
expect(p.includes("..")).toBe(false);
expect(p.startsWith("/")).toBe(false);
expect(existsSync(p)).toBe(true);
}
});
it("skills path points at the shipped skill set", () => {
expect(existsSync(join(manifest.skills, "recall", "SKILL.md"))).toBe(true);
expect(existsSync(join(manifest.skills, "memory-discipline", "SKILL.md"))).toBe(
true,
);
});
});
describe("Cursor plugin hooks config", () => {
it("uses the native format with known event names", () => {
expect(hooks.version).toBe(1);
for (const event of Object.keys(hooks.hooks)) {
expect(CURSOR_HOOK_EVENTS.has(event)).toBe(true);
}
});
it("every hook command resolves from the plugin root to a built script", () => {
for (const entries of Object.values(hooks.hooks) as Array<
Array<{ command: string }>
>) {
for (const entry of entries) {
expect(entry.command).toContain("${CURSOR_PLUGIN_ROOT}/");
const script = entry.command.match(/plugin\/scripts\/\S+\.mjs/)?.[0];
expect(script).toBeDefined();
expect(existsSync(script!)).toBe(true);
}
}
});
});
describe("Cursor plugin MCP config", () => {
it("declares every ${VAR} placeholder in the manifest variables schema", () => {
const raw = readFileSync("plugin/cursor/mcp.json", "utf-8");
const vars = [...raw.matchAll(/\$\{([A-Z0-9_]+)\}/g)].map((m) => m[1]);
expect(vars.length).toBeGreaterThan(0);
for (const v of vars) {
expect(manifest.variables.properties[v]).toBeDefined();
}
});
it("wires the standalone MCP shim over stdio", () => {
const server = mcp.mcpServers.agentmemory;
expect(server.command).toBe("npx");
expect(server.args).toContain("@agentmemory/mcp");
});
});