@stigmer/plugin-package 3.34.0 → 3.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/archive.d.ts.map +1 -1
- package/client/archive.js.map +1 -1
- package/client/builtin.d.ts.map +1 -1
- package/client/builtin.js.map +1 -1
- package/client/ignore/defaults.d.ts.map +1 -1
- package/client/ignore/defaults.js.map +1 -1
- package/client/ignore/match.d.ts.map +1 -1
- package/client/ignore/match.js.map +1 -1
- package/client/ignore/matcher.d.ts.map +1 -1
- package/client/ignore/matcher.js.map +1 -1
- package/client/ignore/pattern.d.ts.map +1 -1
- package/client/ignore/pattern.js.map +1 -1
- package/client/prepare.d.ts.map +1 -1
- package/client/prepare.js.map +1 -1
- package/client/presentation.d.ts.map +1 -1
- package/client/presentation.js.map +1 -1
- package/client/refs.d.ts.map +1 -1
- package/client/refs.js.map +1 -1
- package/client/release.d.ts.map +1 -1
- package/client/release.js.map +1 -1
- package/client/reroot.d.ts.map +1 -1
- package/client/reroot.js.map +1 -1
- package/client/select.d.ts.map +1 -1
- package/client/select.js.map +1 -1
- package/client/vocabulary.d.ts.map +1 -1
- package/client/vocabulary.js.map +1 -1
- package/client.d.ts.map +1 -1
- package/client.js.map +1 -1
- package/detect.d.ts.map +1 -1
- package/detect.js.map +1 -1
- package/dialects/claude.d.ts.map +1 -1
- package/dialects/claude.js.map +1 -1
- package/dialects/codex.d.ts.map +1 -1
- package/dialects/codex.js.map +1 -1
- package/dialects/cursor.d.ts.map +1 -1
- package/dialects/cursor.js.map +1 -1
- package/dialects/manifest.d.ts.map +1 -1
- package/dialects/manifest.js.map +1 -1
- package/dialects/open.d.ts.map +1 -1
- package/dialects/open.js.map +1 -1
- package/documents.d.ts.map +1 -1
- package/documents.js.map +1 -1
- package/files.d.ts.map +1 -1
- package/files.js.map +1 -1
- package/frontmatter.d.ts.map +1 -1
- package/frontmatter.js.map +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/marketplace/messages.d.ts.map +1 -1
- package/marketplace/messages.js.map +1 -1
- package/marketplace/outcome.d.ts.map +1 -1
- package/marketplace/outcome.js.map +1 -1
- package/marketplace/read-marketplace.d.ts.map +1 -1
- package/marketplace/read-marketplace.js.map +1 -1
- package/messages.d.ts.map +1 -1
- package/messages.js.map +1 -1
- package/normalise/ignored.d.ts.map +1 -1
- package/normalise/ignored.js.map +1 -1
- package/normalise/mcp-servers.d.ts.map +1 -1
- package/normalise/mcp-servers.js.map +1 -1
- package/normalise/overlay.d.ts.map +1 -1
- package/normalise/overlay.js.map +1 -1
- package/normalise/skills.d.ts.map +1 -1
- package/normalise/skills.js.map +1 -1
- package/normalise/sub-agents.d.ts.map +1 -1
- package/normalise/sub-agents.js.map +1 -1
- package/normalise/variables.d.ts.map +1 -1
- package/normalise/variables.js.map +1 -1
- package/outcome.d.ts.map +1 -1
- package/outcome.js.map +1 -1
- package/package.json +1 -1
- package/placeholders.d.ts.map +1 -1
- package/placeholders.js.map +1 -1
- package/presentation.d.ts.map +1 -1
- package/presentation.js.map +1 -1
- package/read-plugin-package.d.ts.map +1 -1
- package/read-plugin-package.js.map +1 -1
- package/testing.d.ts.map +1 -1
- package/testing.js.map +1 -1
- package/types.d.ts.map +1 -1
- package/types.js.map +1 -1
- package/src/__test-utils__/directory-files.ts +0 -29
- package/src/__test-utils__/read.ts +0 -55
- package/src/__tests__/adversarial.test.ts +0 -434
- package/src/__tests__/client-ignore.test.ts +0 -133
- package/src/__tests__/client-prepare.test.ts +0 -163
- package/src/__tests__/client-refs.test.ts +0 -101
- package/src/__tests__/client-select-archive.test.ts +0 -150
- package/src/__tests__/detect.test.ts +0 -144
- package/src/__tests__/files.test.ts +0 -119
- package/src/__tests__/fixtures/cursor-plugins/.cursor-plugin/marketplace.json +0 -412
- package/src/__tests__/fixtures/cursor-plugins/NOTICE +0 -27
- package/src/__tests__/fixtures/cursor-plugins/advisor/.cursor-plugin/plugin.json +0 -33
- package/src/__tests__/fixtures/cursor-plugins/advisor/CHANGELOG.md +0 -8
- package/src/__tests__/fixtures/cursor-plugins/advisor/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/advisor/README.md +0 -87
- package/src/__tests__/fixtures/cursor-plugins/advisor/agents/advisor-subagent.md +0 -48
- package/src/__tests__/fixtures/cursor-plugins/advisor/assets/avatar.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/capture-response.sh +0 -20
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/hooks.json +0 -27
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/lib.sh +0 -61
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/mark-pending.sh +0 -27
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/record-consult.sh +0 -41
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/stop-hook.sh +0 -48
- package/src/__tests__/fixtures/cursor-plugins/advisor/skills/advisor/SKILL.md +0 -123
- package/src/__tests__/fixtures/cursor-plugins/advisor/skills/advisor/references/briefing-template.md +0 -44
- package/src/__tests__/fixtures/cursor-plugins/github/.cursor-plugin/plugin.json +0 -45
- package/src/__tests__/fixtures/cursor-plugins/github/CHANGELOG.md +0 -9
- package/src/__tests__/fixtures/cursor-plugins/github/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/github/README.md +0 -64
- package/src/__tests__/fixtures/cursor-plugins/github/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/github/mcp.json +0 -11
- package/src/__tests__/fixtures/cursor-plugins/playwright/.cursor-plugin/plugin.json +0 -35
- package/src/__tests__/fixtures/cursor-plugins/playwright/CHANGELOG.md +0 -8
- package/src/__tests__/fixtures/cursor-plugins/playwright/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/playwright/README.md +0 -46
- package/src/__tests__/fixtures/cursor-plugins/playwright/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/mcp.json +0 -8
- package/src/__tests__/fixtures/cursor-plugins/salesforce/.cursor-plugin/plugin.json +0 -48
- package/src/__tests__/fixtures/cursor-plugins/salesforce/CHANGELOG.md +0 -10
- package/src/__tests__/fixtures/cursor-plugins/salesforce/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/salesforce/README.md +0 -95
- package/src/__tests__/fixtures/cursor-plugins/salesforce/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/mcp.json +0 -12
- package/src/__tests__/fixtures/cursor-plugins/thermos/.cursor-plugin/plugin.json +0 -32
- package/src/__tests__/fixtures/cursor-plugins/thermos/CHANGELOG.md +0 -8
- package/src/__tests__/fixtures/cursor-plugins/thermos/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/thermos/README.md +0 -70
- package/src/__tests__/fixtures/cursor-plugins/thermos/agents/thermo-nuclear-code-quality-review-subagent.md +0 -23
- package/src/__tests__/fixtures/cursor-plugins/thermos/agents/thermo-nuclear-review-subagent.md +0 -28
- package/src/__tests__/fixtures/cursor-plugins/thermos/assets/logo.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermo-nuclear-code-quality-review/SKILL.md +0 -192
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermo-nuclear-review/SKILL.md +0 -51
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermos/SKILL.md +0 -21
- package/src/__tests__/fixtures/cursor-plugins/xero/.cursor-plugin/plugin.json +0 -53
- package/src/__tests__/fixtures/cursor-plugins/xero/CHANGELOG.md +0 -9
- package/src/__tests__/fixtures/cursor-plugins/xero/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/xero/README.md +0 -79
- package/src/__tests__/fixtures/cursor-plugins/xero/assets/logo.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/mcp.json +0 -16
- package/src/__tests__/fixtures.test.ts +0 -198
- package/src/__tests__/marketplace.test.ts +0 -378
- package/src/__tests__/mcp-servers.test.ts +0 -120
- package/src/__tests__/overlay-and-ignored.test.ts +0 -65
- package/src/__tests__/presentation.test.ts +0 -186
- package/src/__tests__/skills.test.ts +0 -89
- package/src/__tests__/sub-agents.test.ts +0 -111
- package/src/__tests__/variables.test.ts +0 -94
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The presentation read, the one a storefront card runs.
|
|
3
|
-
*
|
|
4
|
-
* Pins: the vendored Cursor fixtures yield their `displayName`, `logo`,
|
|
5
|
-
* `category` and author (thermos, a PNG; playwright, an SVG); a Codex
|
|
6
|
-
* manifest yields the same fields from `interface`, with the `./` spelling
|
|
7
|
-
* stripped; a Claude manifest yields identity only, and so does an open
|
|
8
|
-
* manifest until it carries `extensions["ai.stigmer"]`, which speaks the
|
|
9
|
-
* same three fields under the same logo rules; a
|
|
10
|
-
* root manifest names the plugin ahead of a vendor manifest beside it,
|
|
11
|
-
* while the vendor manifest still lends its appearance; a logo that is
|
|
12
|
-
* absent from the listing, over the cap, not an image, a URL or an escape
|
|
13
|
-
* is dropped without a finding; an unparseable manifest contributes nothing
|
|
14
|
-
* and the read still answers. The lazy twin reads exactly the manifests
|
|
15
|
-
* listed and no other file, and treats a fetch that rejects as a manifest
|
|
16
|
-
* that is not there.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
|
|
21
|
-
import { describe, expect, it } from "vitest";
|
|
22
|
-
|
|
23
|
-
import { type LazyCandidate } from "../client/prepare.js";
|
|
24
|
-
import { readPluginPresentationFromTree } from "../client/presentation.js";
|
|
25
|
-
import { inMemoryPluginFiles, PLUGIN_DOCUMENT_LIMITS } from "../files.js";
|
|
26
|
-
import { MANIFEST_LOCATIONS } from "../messages.js";
|
|
27
|
-
import { readPluginPresentation } from "../presentation.js";
|
|
28
|
-
import { claudePlugin, codexPlugin, cursorPlugin, openPlugin, withFile, type PluginFixture } from "../testing.js";
|
|
29
|
-
import { directoryPluginFiles } from "../__test-utils__/directory-files.js";
|
|
30
|
-
|
|
31
|
-
const FIXTURES = fileURLToPath(new URL("./fixtures/cursor-plugins/", import.meta.url));
|
|
32
|
-
const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
33
|
-
|
|
34
|
-
function present(files: PluginFixture) {
|
|
35
|
-
return readPluginPresentation(inMemoryPluginFiles(files));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe("readPluginPresentation over the vendored Cursor fixtures", () => {
|
|
39
|
-
it("thermos: display name, PNG logo, category, author", () => {
|
|
40
|
-
const presentation = readPluginPresentation(directoryPluginFiles(`${FIXTURES}thermos`));
|
|
41
|
-
expect(presentation).toEqual({
|
|
42
|
-
displayName: "Thermos",
|
|
43
|
-
logo: "assets/logo.png",
|
|
44
|
-
category: "developer-tools",
|
|
45
|
-
version: "1.0.0",
|
|
46
|
-
description: expect.stringContaining("Thermo-nuclear branch review"),
|
|
47
|
-
author: { name: "Cursor", email: "plugins@cursor.com" },
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("playwright: an SVG logo is an image too", () => {
|
|
52
|
-
const presentation = readPluginPresentation(directoryPluginFiles(`${FIXTURES}playwright`));
|
|
53
|
-
expect(presentation.displayName).toBe("Playwright");
|
|
54
|
-
expect(presentation.logo).toBe("assets/logo.svg");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("the dialects that carry appearance", () => {
|
|
59
|
-
it("a Cursor manifest speaks at its top level", () => {
|
|
60
|
-
const files = withFile(
|
|
61
|
-
cursorPlugin({ manifest: { displayName: "GitHub", logo: "assets/logo.png", category: "integrations", author: { name: "Cursor" } } }),
|
|
62
|
-
"assets/logo.png",
|
|
63
|
-
PNG_BYTES,
|
|
64
|
-
);
|
|
65
|
-
expect(present(files)).toMatchObject({ displayName: "GitHub", logo: "assets/logo.png", category: "integrations", author: { name: "Cursor" } });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("a legacy Codex manifest speaks through `interface`, and `./` is stripped from its logo", () => {
|
|
69
|
-
const files = withFile(
|
|
70
|
-
codexPlugin({
|
|
71
|
-
version: "6.0.1",
|
|
72
|
-
manifest: { interface: { displayName: "Airtable", logo: "./assets/logo.png", category: "Productivity", developerName: "Airtable" } },
|
|
73
|
-
}),
|
|
74
|
-
"assets/logo.png",
|
|
75
|
-
PNG_BYTES,
|
|
76
|
-
);
|
|
77
|
-
expect(present(files)).toEqual({ displayName: "Airtable", logo: "assets/logo.png", category: "Productivity", version: "6.0.1" });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("a Claude manifest and an open manifest without the extension yield identity only", () => {
|
|
81
|
-
expect(present(claudePlugin({ version: "1.0.0", description: "Reviews pull requests.", manifest: { author: { name: "Anthropic" } } }))).toEqual({
|
|
82
|
-
version: "1.0.0",
|
|
83
|
-
description: "Reviews pull requests.",
|
|
84
|
-
author: { name: "Anthropic" },
|
|
85
|
-
});
|
|
86
|
-
const open = present(openPlugin({ version: "2.0.0", description: "The assistant." }));
|
|
87
|
-
expect(open.displayName).toBeUndefined();
|
|
88
|
-
expect(open.logo).toBeUndefined();
|
|
89
|
-
expect(open.version).toBe("2.0.0");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("an open manifest speaks through extensions[\"ai.stigmer\"], under the same logo rules as a vendor manifest", () => {
|
|
93
|
-
const files = withFile(
|
|
94
|
-
openPlugin({
|
|
95
|
-
version: "1.0.0",
|
|
96
|
-
description: "Linear's hosted MCP server.",
|
|
97
|
-
manifest: { author: { name: "Stigmer" }, extensions: { "ai.stigmer": { displayName: "Linear", logo: "assets/logo.png", category: "integrations" } } },
|
|
98
|
-
}),
|
|
99
|
-
"assets/logo.png",
|
|
100
|
-
PNG_BYTES,
|
|
101
|
-
);
|
|
102
|
-
expect(present(files)).toEqual({
|
|
103
|
-
displayName: "Linear",
|
|
104
|
-
logo: "assets/logo.png",
|
|
105
|
-
category: "integrations",
|
|
106
|
-
version: "1.0.0",
|
|
107
|
-
description: "Linear's hosted MCP server.",
|
|
108
|
-
author: { name: "Stigmer" },
|
|
109
|
-
});
|
|
110
|
-
const unlisted = present(openPlugin({ manifest: { extensions: { "ai.stigmer": { displayName: "Linear", logo: "assets/missing.png" } } } }));
|
|
111
|
-
expect(unlisted).toEqual({ displayName: "Linear" });
|
|
112
|
-
expect(present(openPlugin({ manifest: { extensions: { "ai.stigmer": "nope" } } }))).toEqual({});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("a root manifest names the plugin first; a vendor manifest beside it still lends its appearance", () => {
|
|
116
|
-
let files = openPlugin({ version: "3.0.0", description: "From the root manifest." });
|
|
117
|
-
files = withFile(
|
|
118
|
-
files,
|
|
119
|
-
MANIFEST_LOCATIONS.cursor,
|
|
120
|
-
JSON.stringify({ name: "plugin", version: "1.0.0", description: "From the Cursor manifest.", displayName: "Plugin", logo: "assets/logo.png" }),
|
|
121
|
-
);
|
|
122
|
-
files = withFile(files, "assets/logo.png", PNG_BYTES);
|
|
123
|
-
expect(present(files)).toMatchObject({ version: "3.0.0", description: "From the root manifest.", displayName: "Plugin", logo: "assets/logo.png" });
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("a logo that cannot be shown is no logo, never a finding", () => {
|
|
128
|
-
const cases: readonly [string, string, PluginFixture | undefined][] = [
|
|
129
|
-
["not listed", "assets/missing.png", undefined],
|
|
130
|
-
["a URL", "https://example.com/logo.png", undefined],
|
|
131
|
-
["an escape", "../logo.png", undefined],
|
|
132
|
-
["not an image", "assets/logo.txt", withFile(cursorPlugin(), "assets/logo.txt", "text")],
|
|
133
|
-
["over the cap", "assets/huge.png", withFile(cursorPlugin(), "assets/huge.png", new Uint8Array(PLUGIN_DOCUMENT_LIMITS.logo + 1))],
|
|
134
|
-
];
|
|
135
|
-
for (const [label, logo, base] of cases) {
|
|
136
|
-
it(label, () => {
|
|
137
|
-
const files = withFile(base ?? cursorPlugin(), MANIFEST_LOCATIONS.cursor, JSON.stringify({ name: "plugin", displayName: "Plugin", logo }));
|
|
138
|
-
const presentation = present(files);
|
|
139
|
-
expect(presentation.displayName).toBe("Plugin");
|
|
140
|
-
expect(presentation.logo).toBeUndefined();
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
it("a wrong type is absent", () => {
|
|
145
|
-
const files = withFile(cursorPlugin(), MANIFEST_LOCATIONS.cursor, JSON.stringify({ name: "plugin", displayName: 7, logo: ["assets/logo.png"], author: "Cursor" }));
|
|
146
|
-
expect(present(files)).toEqual({});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("an unparseable manifest contributes nothing and the read answers", () => {
|
|
150
|
-
const files = withFile(cursorPlugin({ version: "1.0.0" }), MANIFEST_LOCATIONS.cursor, "{ not json");
|
|
151
|
-
expect(present(files)).toEqual({});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe("readPluginPresentationFromTree", () => {
|
|
156
|
-
function lazy(files: PluginFixture, reads: string[], failing: ReadonlySet<string> = new Set()): LazyCandidate[] {
|
|
157
|
-
const encoder = new TextEncoder();
|
|
158
|
-
return [...files.entries()].map(([path, content]) => {
|
|
159
|
-
const bytes = typeof content === "string" ? encoder.encode(content) : content;
|
|
160
|
-
return {
|
|
161
|
-
path,
|
|
162
|
-
size: bytes.length,
|
|
163
|
-
read: async () => {
|
|
164
|
-
reads.push(path);
|
|
165
|
-
if (failing.has(path)) throw new Error(`cannot read ${path}`);
|
|
166
|
-
return bytes;
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
it("reads exactly the manifests listed and no other file", async () => {
|
|
173
|
-
const reads: string[] = [];
|
|
174
|
-
const files = withFile(cursorPlugin({ manifest: { displayName: "Thermos", logo: "assets/logo.png" }, skills: [{ name: "review" }] }), "assets/logo.png", PNG_BYTES);
|
|
175
|
-
const presentation = await readPluginPresentationFromTree(lazy(files, reads));
|
|
176
|
-
expect(presentation).toMatchObject({ displayName: "Thermos", logo: "assets/logo.png" });
|
|
177
|
-
expect(reads).toEqual([MANIFEST_LOCATIONS.cursor]);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("a manifest whose fetch rejects is a manifest that is not there", async () => {
|
|
181
|
-
const reads: string[] = [];
|
|
182
|
-
const files = cursorPlugin({ manifest: { displayName: "Thermos" } });
|
|
183
|
-
const presentation = await readPluginPresentationFromTree(lazy(files, reads, new Set([MANIFEST_LOCATIONS.cursor])));
|
|
184
|
-
expect(presentation).toEqual({});
|
|
185
|
-
});
|
|
186
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins skill discovery across the dialects (the fixed `skills/` children,
|
|
3
|
-
* declared paths that are a skill or a directory of skills, Claude's root
|
|
4
|
-
* single skill) and the frontmatter check's relaxations (name defaulted to
|
|
5
|
-
* the directory, name differing from the directory, description missing),
|
|
6
|
-
* plus the per-skill file listing the installer builds archives from.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, expect, it } from "vitest";
|
|
10
|
-
|
|
11
|
-
import { claudePlugin, cursorPlugin, openPlugin, withFile } from "../testing.js";
|
|
12
|
-
import { accepted, kindsOf, read } from "../__test-utils__/read.js";
|
|
13
|
-
|
|
14
|
-
describe("discovery", () => {
|
|
15
|
-
it("reads the immediate children of skills/ that hold SKILL.md, and lists each skill's files", () => {
|
|
16
|
-
const files = openPlugin({
|
|
17
|
-
skills: [
|
|
18
|
-
{ name: "alpha", description: "A", files: { "scripts/run.sh": "echo", "references/REF.md": "ref" } },
|
|
19
|
-
{ name: "beta", description: "B" },
|
|
20
|
-
],
|
|
21
|
-
});
|
|
22
|
-
files.set("skills/not-a-skill/README.md", "no SKILL.md here");
|
|
23
|
-
files.set("skills/alpha/nested/deeper/SKILL.md", "---\nname: deeper\ndescription: d\n---\nbody");
|
|
24
|
-
const plugin = accepted(read(files));
|
|
25
|
-
expect(plugin.skills.map((s) => s.name)).toEqual(["alpha", "beta"]);
|
|
26
|
-
expect(plugin.skills[0]).toMatchObject({
|
|
27
|
-
dir: "skills/alpha",
|
|
28
|
-
files: ["skills/alpha/SKILL.md", "skills/alpha/nested/deeper/SKILL.md", "skills/alpha/references/REF.md", "skills/alpha/scripts/run.sh"],
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("reads a declared directory of skills and a declared path that is itself a skill", () => {
|
|
33
|
-
const files = claudePlugin({ manifest: { skills: ["./extra/", "./solo"] } });
|
|
34
|
-
files.set("extra/one/SKILL.md", "---\nname: one\ndescription: d\n---\nbody");
|
|
35
|
-
files.set("solo/SKILL.md", "---\nname: solo\ndescription: d\n---\nbody");
|
|
36
|
-
files.set("skills/base/SKILL.md", "---\nname: base\ndescription: d\n---\nbody");
|
|
37
|
-
const plugin = accepted(read(files));
|
|
38
|
-
expect(plugin.skills.map((s) => s.dir)).toEqual(["extra/one", "skills/base", "solo"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("reads a root SKILL.md as a single skill when nothing else declares skills", () => {
|
|
42
|
-
const files = claudePlugin({ name: "solo-skill" });
|
|
43
|
-
files.set("SKILL.md", "---\nname: solo-skill\ndescription: d\n---\nbody");
|
|
44
|
-
files.set("README.md", "readme");
|
|
45
|
-
const plugin = accepted(read(files));
|
|
46
|
-
expect(plugin.skills).toHaveLength(1);
|
|
47
|
-
expect(plugin.skills[0]).toMatchObject({ name: "solo-skill", dir: "" });
|
|
48
|
-
expect(plugin.skills[0]?.files).toContain("README.md");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("does not read a root SKILL.md when skills/ exists", () => {
|
|
52
|
-
const files = openPlugin({ skills: [{ name: "a", description: "d" }] });
|
|
53
|
-
files.set("SKILL.md", "---\nname: root\ndescription: d\n---\nbody");
|
|
54
|
-
expect(accepted(read(files)).skills.map((s) => s.name)).toEqual(["a"]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("warns on a declared skills path that names nothing", () => {
|
|
58
|
-
const outcome = read(cursorPlugin({ manifest: { skills: "./missing/" } }));
|
|
59
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: ["path-missing"] });
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("frontmatter", () => {
|
|
64
|
-
it("accepts optional and vendor frontmatter keys silently", () => {
|
|
65
|
-
const files = openPlugin({
|
|
66
|
-
skills: [{ name: "a", description: "d", frontmatter: { license: "MIT", metadata: { author: "x" }, "allowed-tools": "Bash", "disable-model-invocation": true, icon: "bulb" } }],
|
|
67
|
-
});
|
|
68
|
-
expect(kindsOf(read(files))).toEqual({ errors: [], warnings: [] });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("names a skill after its directory when the frontmatter has no name, with a warning", () => {
|
|
72
|
-
const files = withFile(openPlugin(), "skills/from-dir/SKILL.md", "---\ndescription: d\n---\nbody");
|
|
73
|
-
const outcome = read(files);
|
|
74
|
-
expect(kindsOf(outcome).warnings).toEqual(["skill-name-defaulted"]);
|
|
75
|
-
expect(accepted(outcome).skills[0]?.name).toBe("from-dir");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("warns when the name differs from the directory and when the description is missing", () => {
|
|
79
|
-
const files = openPlugin({ skills: [{ name: "real-name", dir: "other-dir" }] });
|
|
80
|
-
const outcome = read(files);
|
|
81
|
-
expect(kindsOf(outcome).warnings).toEqual(["skill-description-missing", "skill-name-differs-from-directory"]);
|
|
82
|
-
expect(accepted(outcome).skills[0]).toMatchObject({ name: "real-name", dir: "skills/other-dir" });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("accepts Stigmer's dot-scoped names, a superset of the Agent Skills rule", () => {
|
|
86
|
-
const plugin = accepted(read(openPlugin({ skills: [{ name: "platform.my-skill", description: "d" }] })));
|
|
87
|
-
expect(plugin.skills[0]?.name).toBe("platform.my-skill");
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins sub-agent normalisation: `agents/*.md` read under vendor manifests
|
|
3
|
-
* and recorded as ignored under a root-only manifest; the body as
|
|
4
|
-
* instructions; name from frontmatter or file stem; Claude `skills:`
|
|
5
|
-
* matched to the plugin's skills; `model` classified into an alias (never
|
|
6
|
-
* an id); every other frontmatter field warned once; declared agent paths
|
|
7
|
-
* that are files or directories.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, expect, it } from "vitest";
|
|
11
|
-
|
|
12
|
-
import { classifyModel } from "../normalise/sub-agents.js";
|
|
13
|
-
import { claudePlugin, cursorPlugin, openPlugin } from "../testing.js";
|
|
14
|
-
import { accepted, kindsOf, read } from "../__test-utils__/read.js";
|
|
15
|
-
|
|
16
|
-
describe("discovery", () => {
|
|
17
|
-
it("reads agents/*.md under a Cursor manifest into SubAgent shapes", () => {
|
|
18
|
-
const plugin = accepted(
|
|
19
|
-
read(
|
|
20
|
-
cursorPlugin({
|
|
21
|
-
agents: [{ file: "reviewer", frontmatter: { name: "code-reviewer", description: "Reviews code." }, body: "Review the diff carefully and report findings.\n" }],
|
|
22
|
-
}),
|
|
23
|
-
),
|
|
24
|
-
);
|
|
25
|
-
expect(plugin.subAgents).toEqual([
|
|
26
|
-
{
|
|
27
|
-
name: "code-reviewer",
|
|
28
|
-
description: "Reviews code.",
|
|
29
|
-
instructions: "Review the diff carefully and report findings.",
|
|
30
|
-
skillNames: [],
|
|
31
|
-
path: "agents/reviewer.md",
|
|
32
|
-
},
|
|
33
|
-
]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("records agents/ as ignored, unread, under a root-only manifest", () => {
|
|
37
|
-
const files = openPlugin();
|
|
38
|
-
files.set("agents/x.md", "---\nname: x\n---\nthis frontmatter would be fine");
|
|
39
|
-
const plugin = accepted(read(files));
|
|
40
|
-
expect(plugin.subAgents).toEqual([]);
|
|
41
|
-
expect(plugin.ignored).toEqual([{ kind: "agents", path: "agents/" }]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("reads declared agent files and directories, replacing the default directory", () => {
|
|
45
|
-
const files = claudePlugin({ manifest: { agents: ["./custom/one.md", "./more/"] } });
|
|
46
|
-
files.set("custom/one.md", "---\nname: one\n---\nDo the first thing well.");
|
|
47
|
-
files.set("more/two.md", "---\nname: two\n---\nDo the second thing well.");
|
|
48
|
-
files.set("agents/ignored.md", "---\nname: three\n---\nNot read: the manifest replaced agents/.");
|
|
49
|
-
const plugin = accepted(read(files));
|
|
50
|
-
expect(plugin.subAgents.map((a) => a.name)).toEqual(["one", "two"]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("names a bare prompt after its file with a warning", () => {
|
|
54
|
-
const outcome = read(claudePlugin({ agents: [{ file: "helper", frontmatter: null, body: "You help with everything, patiently." }] }));
|
|
55
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: ["sub-agent-name-defaulted"] });
|
|
56
|
-
expect(accepted(outcome).subAgents[0]).toMatchObject({ name: "helper", instructions: "You help with everything, patiently." });
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("frontmatter fields", () => {
|
|
61
|
-
it("matches Claude skills: to the plugin's skills and warns on the rest", () => {
|
|
62
|
-
const outcome = read(
|
|
63
|
-
claudePlugin({
|
|
64
|
-
skills: [{ name: "known", description: "d" }],
|
|
65
|
-
agents: [{ file: "a", frontmatter: { skills: ["known", "unknown"] } }],
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
expect(kindsOf(outcome).warnings).toEqual(["sub-agent-skill-unknown"]);
|
|
69
|
-
expect(accepted(outcome).subAgents[0]?.skillNames).toEqual(["known"]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("classifies model into an alias and warns on one it cannot map", () => {
|
|
73
|
-
const outcome = read(
|
|
74
|
-
cursorPlugin({
|
|
75
|
-
agents: [
|
|
76
|
-
{ file: "fast", frontmatter: { model: "fast" } },
|
|
77
|
-
{ file: "sonnet", frontmatter: { model: "claude-sonnet-4-5" } },
|
|
78
|
-
{ file: "inherit", frontmatter: { model: "inherit" } },
|
|
79
|
-
{ file: "grok", frontmatter: { model: "grok-4.6[effort=xhigh]" } },
|
|
80
|
-
{ file: "none", frontmatter: {} },
|
|
81
|
-
],
|
|
82
|
-
}),
|
|
83
|
-
);
|
|
84
|
-
expect(kindsOf(outcome).warnings).toEqual(["sub-agent-model-unknown"]);
|
|
85
|
-
const hints = accepted(outcome).subAgents.map((a) => [a.name, a.modelHint?.alias]);
|
|
86
|
-
expect(hints).toEqual([
|
|
87
|
-
["fast", "fast"],
|
|
88
|
-
["grok", "unknown"],
|
|
89
|
-
["inherit", "inherit"],
|
|
90
|
-
["none", undefined],
|
|
91
|
-
["sonnet", "sonnet"],
|
|
92
|
-
]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("warns once per field Stigmer does not read", () => {
|
|
96
|
-
const outcome = read(cursorPlugin({ agents: [{ file: "a", frontmatter: { readonly: true, is_background: false, effort: "high" } }] }));
|
|
97
|
-
expect(kindsOf(outcome).warnings).toEqual(["sub-agent-field-ignored", "sub-agent-field-ignored", "sub-agent-field-ignored"]);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("classifyModel", () => {
|
|
102
|
-
it("maps the dialects' spellings onto the alias set", () => {
|
|
103
|
-
expect(classifyModel("fast").alias).toBe("fast");
|
|
104
|
-
expect(classifyModel("Sonnet").alias).toBe("sonnet");
|
|
105
|
-
expect(classifyModel("claude-opus-4-1").alias).toBe("opus");
|
|
106
|
-
expect(classifyModel("claude-haiku-4.5").alias).toBe("haiku");
|
|
107
|
-
expect(classifyModel("inherit").alias).toBe("inherit");
|
|
108
|
-
expect(classifyModel("gpt-5").alias).toBe("unknown");
|
|
109
|
-
expect(classifyModel("gpt-5").raw).toBe("gpt-5");
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins variable normalisation: Cursor variables are secrets with `required`
|
|
3
|
-
* deciding `optional`, Claude `userConfig` carries `sensitive` and
|
|
4
|
-
* `required`, titles and descriptions are joined, the unsupported Claude
|
|
5
|
-
* attributes warn once each, an undeclared reference is declared as a
|
|
6
|
-
* required secret with a warning naming the server, and a declared
|
|
7
|
-
* variable no server uses is warned.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, expect, it } from "vitest";
|
|
11
|
-
|
|
12
|
-
import { claudePlugin, cursorPlugin } from "../testing.js";
|
|
13
|
-
import { accepted, findingOf, kindsOf, read } from "../__test-utils__/read.js";
|
|
14
|
-
|
|
15
|
-
describe("Cursor variables", () => {
|
|
16
|
-
it("declares every property as a secret, optional unless required", () => {
|
|
17
|
-
const plugin = accepted(
|
|
18
|
-
read(
|
|
19
|
-
cursorPlugin({
|
|
20
|
-
variables: {
|
|
21
|
-
TOKEN: { type: "string", title: "Token", description: "A personal access token." },
|
|
22
|
-
REGION: { type: "string", title: "Region" },
|
|
23
|
-
},
|
|
24
|
-
required: ["TOKEN"],
|
|
25
|
-
mcpServers: { s: { type: "http", url: "https://x.example.com/mcp", headers: { A: "${TOKEN}", B: "${REGION}" } } },
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
);
|
|
29
|
-
expect(plugin.variables).toEqual([
|
|
30
|
-
{ name: "TOKEN", description: "Token: A personal access token.", isSecret: true, optional: false, declaredBy: "cursor" },
|
|
31
|
-
{ name: "REGION", description: "Region", isSecret: true, optional: true, declaredBy: "cursor" },
|
|
32
|
-
]);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe("Claude userConfig", () => {
|
|
37
|
-
it("carries sensitive and required, and warns once per unsupported attribute", () => {
|
|
38
|
-
const outcome = read(
|
|
39
|
-
claudePlugin({
|
|
40
|
-
userConfig: {
|
|
41
|
-
API_KEY: { type: "string", title: "API key", sensitive: true, required: true },
|
|
42
|
-
MODE: { type: "string", default: "fast", options: ["fast", "slow"] },
|
|
43
|
-
DIR: { type: "directory", description: "Where to write" },
|
|
44
|
-
COUNT: { type: "number", min: 1, max: 5, multiple: true },
|
|
45
|
-
},
|
|
46
|
-
mcpServers: {
|
|
47
|
-
s: { command: "npx", args: ["x", "${MODE}", "${DIR}", "${COUNT}"], env: { API_KEY: "${API_KEY}" } },
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
expect(kindsOf(outcome).warnings).toEqual([
|
|
52
|
-
"variable-default-dropped",
|
|
53
|
-
"variable-option-dropped",
|
|
54
|
-
"variable-option-dropped",
|
|
55
|
-
"variable-option-dropped",
|
|
56
|
-
"variable-option-dropped",
|
|
57
|
-
"variable-type-narrowed",
|
|
58
|
-
"variable-type-narrowed",
|
|
59
|
-
]);
|
|
60
|
-
expect(accepted(outcome).variables).toEqual([
|
|
61
|
-
{ name: "API_KEY", description: "API key", isSecret: true, optional: false, declaredBy: "claude" },
|
|
62
|
-
{ name: "MODE", isSecret: false, optional: true, declaredBy: "claude" },
|
|
63
|
-
{ name: "DIR", description: "Where to write", isSecret: false, optional: true, declaredBy: "claude" },
|
|
64
|
-
{ name: "COUNT", isSecret: false, optional: true, declaredBy: "claude" },
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("reconciliation with references", () => {
|
|
70
|
-
it("declares an undeclared reference as a required secret and names the server", () => {
|
|
71
|
-
const outcome = read(cursorPlugin({ mcpServers: { gh: { type: "http", url: "https://api.example.com/mcp", headers: { Authorization: "Bearer ${GITHUB_TOKEN}" } } } }));
|
|
72
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: ["variable-inferred"] });
|
|
73
|
-
expect(findingOf(outcome.warnings, "variable-inferred")).toMatchObject({ subject: "GITHUB_TOKEN", detail: "gh" });
|
|
74
|
-
expect(accepted(outcome).variables).toEqual([{ name: "GITHUB_TOKEN", isSecret: true, optional: false, declaredBy: "inferred" }]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("warns on a declared variable no server references", () => {
|
|
78
|
-
const outcome = read(cursorPlugin({ variables: { UNUSED: { type: "string" } } }));
|
|
79
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: ["variable-unreferenced"] });
|
|
80
|
-
expect(accepted(outcome).variables[0]?.name).toBe("UNUSED");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("lists declared variables first in declaration order, then inferred ones sorted", () => {
|
|
84
|
-
const plugin = accepted(
|
|
85
|
-
read(
|
|
86
|
-
cursorPlugin({
|
|
87
|
-
variables: { Z: { type: "string" }, A: { type: "string" } },
|
|
88
|
-
mcpServers: { s: { type: "http", url: "https://x.example.com/mcp", headers: { h: "${Z} ${A} ${M} ${B}" } } },
|
|
89
|
-
}),
|
|
90
|
-
),
|
|
91
|
-
);
|
|
92
|
-
expect(plugin.variables.map((v) => v.name)).toEqual(["Z", "A", "B", "M"]);
|
|
93
|
-
});
|
|
94
|
-
});
|