@stigmer/plugin-package 3.15.3-dev.20260916211208
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/LICENSE +190 -0
- package/README.md +66 -0
- package/detect.d.ts +50 -0
- package/detect.d.ts.map +1 -0
- package/detect.js +164 -0
- package/detect.js.map +1 -0
- package/dialects/claude.d.ts +30 -0
- package/dialects/claude.d.ts.map +1 -0
- package/dialects/claude.js +71 -0
- package/dialects/claude.js.map +1 -0
- package/dialects/codex.d.ts +18 -0
- package/dialects/codex.d.ts.map +1 -0
- package/dialects/codex.js +19 -0
- package/dialects/codex.js.map +1 -0
- package/dialects/cursor.d.ts +23 -0
- package/dialects/cursor.d.ts.map +1 -0
- package/dialects/cursor.js +63 -0
- package/dialects/cursor.js.map +1 -0
- package/dialects/manifest.d.ts +109 -0
- package/dialects/manifest.d.ts.map +1 -0
- package/dialects/manifest.js +194 -0
- package/dialects/manifest.js.map +1 -0
- package/dialects/open.d.ts +18 -0
- package/dialects/open.d.ts.map +1 -0
- package/dialects/open.js +47 -0
- package/dialects/open.js.map +1 -0
- package/documents.d.ts +43 -0
- package/documents.d.ts.map +1 -0
- package/documents.js +111 -0
- package/documents.js.map +1 -0
- package/files.d.ts +114 -0
- package/files.d.ts.map +1 -0
- package/files.js +187 -0
- package/files.js.map +1 -0
- package/frontmatter.d.ts +51 -0
- package/frontmatter.d.ts.map +1 -0
- package/frontmatter.js +61 -0
- package/frontmatter.js.map +1 -0
- package/index.d.ts +19 -0
- package/index.d.ts.map +1 -0
- package/index.js +17 -0
- package/index.js.map +1 -0
- package/messages.d.ts +39 -0
- package/messages.d.ts.map +1 -0
- package/messages.js +135 -0
- package/messages.js.map +1 -0
- package/normalise/ignored.d.ts +24 -0
- package/normalise/ignored.d.ts.map +1 -0
- package/normalise/ignored.js +68 -0
- package/normalise/ignored.js.map +1 -0
- package/normalise/mcp-servers.d.ts +47 -0
- package/normalise/mcp-servers.d.ts.map +1 -0
- package/normalise/mcp-servers.js +397 -0
- package/normalise/mcp-servers.js.map +1 -0
- package/normalise/overlay.d.ts +27 -0
- package/normalise/overlay.d.ts.map +1 -0
- package/normalise/overlay.js +67 -0
- package/normalise/overlay.js.map +1 -0
- package/normalise/skills.d.ts +31 -0
- package/normalise/skills.d.ts.map +1 -0
- package/normalise/skills.js +116 -0
- package/normalise/skills.js.map +1 -0
- package/normalise/sub-agents.d.ts +44 -0
- package/normalise/sub-agents.d.ts.map +1 -0
- package/normalise/sub-agents.js +168 -0
- package/normalise/sub-agents.js.map +1 -0
- package/normalise/variables.d.ts +27 -0
- package/normalise/variables.d.ts.map +1 -0
- package/normalise/variables.js +155 -0
- package/normalise/variables.js.map +1 -0
- package/outcome.d.ts +46 -0
- package/outcome.d.ts.map +1 -0
- package/outcome.js +21 -0
- package/outcome.js.map +1 -0
- package/package.json +40 -0
- package/placeholders.d.ts +41 -0
- package/placeholders.d.ts.map +1 -0
- package/placeholders.js +68 -0
- package/placeholders.js.map +1 -0
- package/read-plugin-package.d.ts +19 -0
- package/read-plugin-package.d.ts.map +1 -0
- package/read-plugin-package.js +67 -0
- package/read-plugin-package.js.map +1 -0
- package/src/__test-utils__/directory-files.ts +29 -0
- package/src/__test-utils__/read.ts +55 -0
- package/src/__tests__/adversarial.test.ts +434 -0
- package/src/__tests__/detect.test.ts +133 -0
- package/src/__tests__/files.test.ts +119 -0
- package/src/__tests__/fixtures/cursor-plugins/NOTICE +22 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/.cursor-plugin/plugin.json +33 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/CHANGELOG.md +8 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/LICENSE +21 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/README.md +87 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/agents/advisor-subagent.md +48 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/assets/avatar.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/capture-response.sh +20 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/hooks.json +27 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/lib.sh +61 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/mark-pending.sh +27 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/record-consult.sh +41 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/stop-hook.sh +48 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/skills/advisor/SKILL.md +123 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/skills/advisor/references/briefing-template.md +44 -0
- package/src/__tests__/fixtures/cursor-plugins/github/.cursor-plugin/plugin.json +45 -0
- package/src/__tests__/fixtures/cursor-plugins/github/CHANGELOG.md +9 -0
- package/src/__tests__/fixtures/cursor-plugins/github/LICENSE +21 -0
- package/src/__tests__/fixtures/cursor-plugins/github/README.md +64 -0
- package/src/__tests__/fixtures/cursor-plugins/github/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/github/mcp.json +11 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/.cursor-plugin/plugin.json +35 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/CHANGELOG.md +8 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/LICENSE +21 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/README.md +46 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/mcp.json +8 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/.cursor-plugin/plugin.json +48 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/CHANGELOG.md +10 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/LICENSE +21 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/README.md +95 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/mcp.json +12 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/.cursor-plugin/plugin.json +32 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/CHANGELOG.md +8 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/LICENSE +21 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/README.md +70 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/agents/thermo-nuclear-code-quality-review-subagent.md +23 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/agents/thermo-nuclear-review-subagent.md +28 -0
- 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 +192 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermo-nuclear-review/SKILL.md +51 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermos/SKILL.md +21 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/.cursor-plugin/plugin.json +53 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/CHANGELOG.md +9 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/LICENSE +21 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/README.md +79 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/assets/logo.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/mcp.json +16 -0
- package/src/__tests__/fixtures.test.ts +198 -0
- package/src/__tests__/mcp-servers.test.ts +120 -0
- package/src/__tests__/overlay-and-ignored.test.ts +65 -0
- package/src/__tests__/skills.test.ts +89 -0
- package/src/__tests__/sub-agents.test.ts +111 -0
- package/src/__tests__/variables.test.ts +94 -0
- package/src/detect.ts +189 -0
- package/src/dialects/claude.ts +90 -0
- package/src/dialects/codex.ts +24 -0
- package/src/dialects/cursor.ts +73 -0
- package/src/dialects/manifest.ts +237 -0
- package/src/dialects/open.ts +48 -0
- package/src/documents.ts +145 -0
- package/src/files.ts +213 -0
- package/src/frontmatter.ts +70 -0
- package/src/index.ts +59 -0
- package/src/messages.ts +206 -0
- package/src/normalise/ignored.ts +70 -0
- package/src/normalise/mcp-servers.ts +427 -0
- package/src/normalise/overlay.ts +71 -0
- package/src/normalise/skills.ts +126 -0
- package/src/normalise/sub-agents.ts +184 -0
- package/src/normalise/variables.ts +161 -0
- package/src/outcome.ts +122 -0
- package/src/placeholders.ts +74 -0
- package/src/read-plugin-package.ts +74 -0
- package/src/testing.ts +258 -0
- package/src/types.ts +189 -0
- package/testing.d.ts +106 -0
- package/testing.d.ts.map +1 -0
- package/testing.js +182 -0
- package/testing.js.map +1 -0
- package/types.d.ts +152 -0
- package/types.d.ts.map +1 -0
- package/types.js +19 -0
- package/types.js.map +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
});
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which manifests a package carries, which one gives it its identity, and
|
|
3
|
+
* what they agree on.
|
|
4
|
+
*
|
|
5
|
+
* A root `plugin.json` (the open format) is the identity when present, and
|
|
6
|
+
* any vendor manifests beside it contribute only their dialect data
|
|
7
|
+
* (declared paths, variables, inline servers). Without a root manifest the
|
|
8
|
+
* precedence is Claude, Cursor, Codex: the first present names the plugin.
|
|
9
|
+
* Every present manifest is read, so a plugin that ships two dialects has
|
|
10
|
+
* both honoured; a `name` present in two manifests with different values is
|
|
11
|
+
* refused, because a plugin that is two plugins depending on who reads it
|
|
12
|
+
* is exactly the drift Stigmer will not carry.
|
|
13
|
+
*
|
|
14
|
+
* The plugin name obeys the open format's rule in every dialect (1 to 64
|
|
15
|
+
* characters of `a-z 0-9 - .`, alphanumeric at both ends, no `--` or `..`);
|
|
16
|
+
* Cursor's published pattern is the same rule minus the repetition clause.
|
|
17
|
+
* Slugging is the installer's, not the reader's.
|
|
18
|
+
*
|
|
19
|
+
* MCP configuration sources are resolved here too, because a default
|
|
20
|
+
* location is a cross-manifest fact: `mcp.json` is read under the open
|
|
21
|
+
* rules when a root manifest exists; a Claude or Codex manifest that
|
|
22
|
+
* declares no `mcpServers` reads `.mcp.json` when present; Cursor reads
|
|
23
|
+
* only what it declares. Sub-agent files (`agents/*.md`) are a vendor
|
|
24
|
+
* component: they are read when any vendor manifest is present and recorded
|
|
25
|
+
* as ignored otherwise, since the open format defines no such component.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readClaudeManifest, CLAUDE_DEFAULT_MCP_CONFIG } from "./dialects/claude.js";
|
|
29
|
+
import { readCodexManifest } from "./dialects/codex.js";
|
|
30
|
+
import { readCursorManifest } from "./dialects/cursor.js";
|
|
31
|
+
import type { DialectManifest, McpConfigSource } from "./dialects/manifest.js";
|
|
32
|
+
import { readOpenManifest } from "./dialects/open.js";
|
|
33
|
+
import { type JsonObject, parseJsonObject, readText } from "./documents.js";
|
|
34
|
+
import type { PluginFileIndex } from "./files.js";
|
|
35
|
+
import { type Findings, MANIFEST_LOCATIONS } from "./messages.js";
|
|
36
|
+
import type { PluginDialect } from "./types.js";
|
|
37
|
+
|
|
38
|
+
/** The open format's `mcp.json`, a fixed location. */
|
|
39
|
+
export const OPEN_MCP_CONFIG = "mcp.json";
|
|
40
|
+
|
|
41
|
+
/** The vendor precedence when no root manifest exists. */
|
|
42
|
+
const VENDOR_PRECEDENCE: readonly Exclude<PluginDialect, "agent-plugins">[] = ["claude", "cursor", "codex"];
|
|
43
|
+
|
|
44
|
+
const NAME_MAX_LENGTH = 64;
|
|
45
|
+
const NAME_PATTERN = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
|
|
46
|
+
|
|
47
|
+
export interface ManifestSet {
|
|
48
|
+
/** The dialect that gave the plugin its identity. */
|
|
49
|
+
readonly dialect: PluginDialect;
|
|
50
|
+
/** Identity first, then the others in location order. */
|
|
51
|
+
readonly manifests: readonly DialectManifest[];
|
|
52
|
+
/** The validated plugin name; `undefined` after a name finding. */
|
|
53
|
+
readonly name: string | undefined;
|
|
54
|
+
/** True when a vendor manifest is present, so `agents/*.md` is a component. */
|
|
55
|
+
readonly readsAgents: boolean;
|
|
56
|
+
readonly mcpSources: readonly McpConfigSource[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when the directory holds any of the four manifests; the CLI's routing test. */
|
|
60
|
+
export function hasPluginManifest(paths: Iterable<string>): boolean {
|
|
61
|
+
const locations = new Set<string>(Object.values(MANIFEST_LOCATIONS));
|
|
62
|
+
for (const path of paths) if (locations.has(path)) return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read every present manifest; `undefined` (after `no-manifest`) when there is none. */
|
|
67
|
+
export function detectManifests(index: PluginFileIndex, findings: Findings): ManifestSet | undefined {
|
|
68
|
+
const manifests: DialectManifest[] = [];
|
|
69
|
+
const present = (dialect: PluginDialect): DialectManifest | undefined => {
|
|
70
|
+
const path = MANIFEST_LOCATIONS[dialect];
|
|
71
|
+
if (!index.has(path)) return undefined;
|
|
72
|
+
const object = readManifestObject(index, path, findings);
|
|
73
|
+
// An unreadable manifest still counts as present: the finding already
|
|
74
|
+
// says why, and an empty contribution keeps the read going.
|
|
75
|
+
const manifest = object === undefined ? emptyManifest(dialect, path) : readDialect(dialect, object, path, findings);
|
|
76
|
+
manifests.push(manifest);
|
|
77
|
+
return manifest;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const root = present("agent-plugins");
|
|
81
|
+
const vendors = VENDOR_PRECEDENCE.map(present).filter((m): m is DialectManifest => m !== undefined);
|
|
82
|
+
if (manifests.length === 0) {
|
|
83
|
+
findings.error("no-manifest");
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const identity = root ?? vendors[0];
|
|
88
|
+
if (identity === undefined) {
|
|
89
|
+
// Unreachable: manifests is non-empty, so root or vendors[0] exists.
|
|
90
|
+
throw new Error("manifest set without an identity manifest");
|
|
91
|
+
}
|
|
92
|
+
const ordered = [identity, ...manifests.filter((m) => m !== identity)];
|
|
93
|
+
const name = resolveName(identity, ordered, findings);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
dialect: identity.dialect,
|
|
97
|
+
manifests: ordered,
|
|
98
|
+
name,
|
|
99
|
+
readsAgents: vendors.length > 0,
|
|
100
|
+
mcpSources: resolveMcpSources(index, root, vendors),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readManifestObject(index: PluginFileIndex, path: string, findings: Findings): JsonObject | undefined {
|
|
105
|
+
const text = readText(index, path, "manifest", findings);
|
|
106
|
+
return text === undefined ? undefined : parseJsonObject(text, path, "manifest-unreadable", findings);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readDialect(dialect: PluginDialect, object: JsonObject, path: string, findings: Findings): DialectManifest {
|
|
110
|
+
switch (dialect) {
|
|
111
|
+
case "agent-plugins":
|
|
112
|
+
return readOpenManifest(object, path, findings);
|
|
113
|
+
case "claude":
|
|
114
|
+
return readClaudeManifest(object, path, findings);
|
|
115
|
+
case "cursor":
|
|
116
|
+
return readCursorManifest(object, path, findings);
|
|
117
|
+
case "codex":
|
|
118
|
+
return readCodexManifest(object, path, findings);
|
|
119
|
+
default: {
|
|
120
|
+
const exhaustive: never = dialect;
|
|
121
|
+
throw new Error(`unknown dialect ${String(exhaustive)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function emptyManifest(dialect: PluginDialect, path: string): DialectManifest {
|
|
127
|
+
return { dialect, path, identity: {}, skillPaths: [], mcpConfigs: [], ignored: [] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveName(identity: DialectManifest, all: readonly DialectManifest[], findings: Findings): string | undefined {
|
|
131
|
+
const name = identity.identity.name;
|
|
132
|
+
if (name === undefined) {
|
|
133
|
+
// An unreadable identity manifest already reported itself; a second
|
|
134
|
+
// finding about its missing name would blame the author twice.
|
|
135
|
+
if (findings.errors.some((f) => f.kind === "manifest-unreadable" && f.path === identity.path)) return undefined;
|
|
136
|
+
findings.error("manifest-name-missing", { path: identity.path });
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
let valid = true;
|
|
140
|
+
if (!isValidPluginName(name)) {
|
|
141
|
+
findings.error("manifest-name-invalid", { path: identity.path, subject: name });
|
|
142
|
+
valid = false;
|
|
143
|
+
}
|
|
144
|
+
for (const other of all) {
|
|
145
|
+
if (other === identity) continue;
|
|
146
|
+
const otherName = other.identity.name;
|
|
147
|
+
if (otherName !== undefined && otherName !== name) {
|
|
148
|
+
findings.error("manifest-name-conflict", {
|
|
149
|
+
path: identity.path,
|
|
150
|
+
subject: name,
|
|
151
|
+
detail: `'${otherName}' in '${other.path}'`,
|
|
152
|
+
});
|
|
153
|
+
valid = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return valid ? name : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The open format's name rule, applied in every dialect. */
|
|
160
|
+
export function isValidPluginName(name: string): boolean {
|
|
161
|
+
return name.length <= NAME_MAX_LENGTH && NAME_PATTERN.test(name) && !name.includes("--") && !name.includes("..");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveMcpSources(
|
|
165
|
+
index: PluginFileIndex,
|
|
166
|
+
root: DialectManifest | undefined,
|
|
167
|
+
vendors: readonly DialectManifest[],
|
|
168
|
+
): readonly McpConfigSource[] {
|
|
169
|
+
const sources: McpConfigSource[] = [];
|
|
170
|
+
if (root !== undefined && index.has(OPEN_MCP_CONFIG)) {
|
|
171
|
+
sources.push({ kind: "file", path: OPEN_MCP_CONFIG, dialect: "agent-plugins", manifest: root.path });
|
|
172
|
+
}
|
|
173
|
+
for (const vendor of vendors) {
|
|
174
|
+
if (vendor.mcpConfigs.length > 0) {
|
|
175
|
+
sources.push(...vendor.mcpConfigs);
|
|
176
|
+
} else if (vendor.dialect !== "cursor" && index.has(CLAUDE_DEFAULT_MCP_CONFIG)) {
|
|
177
|
+
sources.push({ kind: "file", path: CLAUDE_DEFAULT_MCP_CONFIG, dialect: vendor.dialect, manifest: vendor.path });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Two manifests may name the same file (Claude and Codex both defaulting
|
|
181
|
+
// to `.mcp.json`); reading it twice would report every server twice.
|
|
182
|
+
const seen = new Set<string>();
|
|
183
|
+
return sources.filter((source) => {
|
|
184
|
+
if (source.kind === "inline") return true;
|
|
185
|
+
if (seen.has(source.path)) return false;
|
|
186
|
+
seen.add(source.path);
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code manifest (`.claude-plugin/plugin.json`), and the shape the
|
|
3
|
+
* Codex compatibility manifest shares with it.
|
|
4
|
+
*
|
|
5
|
+
* Claude's manifest is optional in its own tool (without one, the directory
|
|
6
|
+
* name is the plugin name) and `name` is its only required field. Path
|
|
7
|
+
* fields are a string or an array of strings, `./`-prefixed: `skills` ADDS
|
|
8
|
+
* to the default `skills/` scan and may name a directory that is itself a
|
|
9
|
+
* skill; `agents` REPLACES the default `agents/` and its entries are files
|
|
10
|
+
* or directories. `mcpServers` is a path, an inline object, or an array of
|
|
11
|
+
* either, defaulting to `.mcp.json` at the plugin root. `userConfig`
|
|
12
|
+
* declares the variables a user is prompted for. Hooks, LSP servers,
|
|
13
|
+
* channels, output styles, workflows and the experimental components are
|
|
14
|
+
* recorded as ignored.
|
|
15
|
+
*
|
|
16
|
+
* The reader is exported with its known-field set so the Codex dialect,
|
|
17
|
+
* whose legacy manifest is this shape plus `apps` and `interface`, reuses it
|
|
18
|
+
* rather than restating it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { JsonObject } from "../documents.js";
|
|
22
|
+
import type { Findings } from "../messages.js";
|
|
23
|
+
import type { PluginDialect } from "../types.js";
|
|
24
|
+
import {
|
|
25
|
+
type DialectManifest,
|
|
26
|
+
ignoredFieldComponents,
|
|
27
|
+
readDeclaredPaths,
|
|
28
|
+
readIdentity,
|
|
29
|
+
readMcpSources,
|
|
30
|
+
warnUnknownFields,
|
|
31
|
+
} from "./manifest.js";
|
|
32
|
+
|
|
33
|
+
export const CLAUDE_KNOWN_FIELDS: ReadonlySet<string> = new Set([
|
|
34
|
+
"name",
|
|
35
|
+
"version",
|
|
36
|
+
"description",
|
|
37
|
+
"author",
|
|
38
|
+
"homepage",
|
|
39
|
+
"repository",
|
|
40
|
+
"license",
|
|
41
|
+
"keywords",
|
|
42
|
+
"skills",
|
|
43
|
+
"commands",
|
|
44
|
+
"agents",
|
|
45
|
+
"workflows",
|
|
46
|
+
"hooks",
|
|
47
|
+
"mcpServers",
|
|
48
|
+
"outputStyles",
|
|
49
|
+
"lspServers",
|
|
50
|
+
"experimental",
|
|
51
|
+
"userConfig",
|
|
52
|
+
"channels",
|
|
53
|
+
"dependencies",
|
|
54
|
+
"defaultEnabled",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** The default MCP configuration file when the manifest declares none. */
|
|
58
|
+
export const CLAUDE_DEFAULT_MCP_CONFIG = ".mcp.json";
|
|
59
|
+
|
|
60
|
+
export function readClaudeManifest(object: JsonObject, path: string, findings: Findings): DialectManifest {
|
|
61
|
+
return readClaudeShapedManifest(object, path, "claude", CLAUDE_KNOWN_FIELDS, findings);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The Claude shape under a given dialect and known-field set (Codex reuses it). */
|
|
65
|
+
export function readClaudeShapedManifest(
|
|
66
|
+
object: JsonObject,
|
|
67
|
+
path: string,
|
|
68
|
+
dialect: PluginDialect,
|
|
69
|
+
known: ReadonlySet<string>,
|
|
70
|
+
findings: Findings,
|
|
71
|
+
): DialectManifest {
|
|
72
|
+
warnUnknownFields(object, known, path, findings);
|
|
73
|
+
const identity = readIdentity(object, path, findings);
|
|
74
|
+
const skillPaths = readDeclaredPaths(object, "skills", path, findings) ?? [];
|
|
75
|
+
const agentPaths = readDeclaredPaths(object, "agents", path, findings);
|
|
76
|
+
const declaredMcp = readMcpSources(object, "mcpServers", path, dialect, findings);
|
|
77
|
+
const manifest: { -readonly [K in keyof DialectManifest]: DialectManifest[K] } = {
|
|
78
|
+
dialect,
|
|
79
|
+
path,
|
|
80
|
+
identity,
|
|
81
|
+
skillPaths,
|
|
82
|
+
mcpConfigs: declaredMcp,
|
|
83
|
+
ignored: ignoredFieldComponents(object, path),
|
|
84
|
+
};
|
|
85
|
+
if (agentPaths !== undefined) manifest.agentPaths = agentPaths;
|
|
86
|
+
if (object["userConfig"] !== undefined) {
|
|
87
|
+
manifest.variables = { dialect: "claude", value: object["userConfig"], manifest: path };
|
|
88
|
+
}
|
|
89
|
+
return manifest;
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Codex compatibility manifest (`.codex-plugin/plugin.json`).
|
|
3
|
+
*
|
|
4
|
+
* New Codex packages are the open form: a root `plugin.json` with
|
|
5
|
+
* OpenAI-specific presentation, app mappings and hooks under
|
|
6
|
+
* `extensions["com.openai"]`, which the open reader records as an ignored
|
|
7
|
+
* extension. The legacy `.codex-plugin/plugin.json` that Codex still
|
|
8
|
+
* accepts is the Claude shape (`skills`, `mcpServers`, hooks) plus two
|
|
9
|
+
* fields of its own: `apps` (registered ChatGPT app mappings, `.app.json`)
|
|
10
|
+
* and `interface` (marketplace presentation). Nothing Codex-specific maps
|
|
11
|
+
* to a Stigmer resource, so the reader is the Claude reader under the
|
|
12
|
+
* `codex` dialect with the two extra fields known and `apps` ignored.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { JsonObject } from "../documents.js";
|
|
16
|
+
import type { Findings } from "../messages.js";
|
|
17
|
+
import { CLAUDE_KNOWN_FIELDS, readClaudeShapedManifest } from "./claude.js";
|
|
18
|
+
import type { DialectManifest } from "./manifest.js";
|
|
19
|
+
|
|
20
|
+
const KNOWN_FIELDS: ReadonlySet<string> = new Set([...CLAUDE_KNOWN_FIELDS, "apps", "interface"]);
|
|
21
|
+
|
|
22
|
+
export function readCodexManifest(object: JsonObject, path: string, findings: Findings): DialectManifest {
|
|
23
|
+
return readClaudeShapedManifest(object, path, "codex", KNOWN_FIELDS, findings);
|
|
24
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cursor manifest (`.cursor-plugin/plugin.json`).
|
|
3
|
+
*
|
|
4
|
+
* Cursor publishes a draft-07 schema for this file (`additionalProperties:
|
|
5
|
+
* false`, only `name` required); the known-field set below is that schema's
|
|
6
|
+
* key list, and `author` there carries `name` and `email` only. Component
|
|
7
|
+
* fields `skills`, `agents`, `rules`, `commands` are a path or an array of
|
|
8
|
+
* paths (the schema says "glob pattern(s) or path(s)"; every published
|
|
9
|
+
* plugin uses a `./`-prefixed directory, and a glob is refused rather than
|
|
10
|
+
* expanded). `mcpServers` is a path, an inline object, or an array of
|
|
11
|
+
* either. `variables` is a JSON Schema object whose `properties` are the
|
|
12
|
+
* variables a user is prompted for and whose `required` list marks the
|
|
13
|
+
* mandatory ones; Cursor has no sensitivity flag, so Stigmer treats every
|
|
14
|
+
* Cursor variable as a secret. `rules`, `commands`, `hooks`, `logo` and
|
|
15
|
+
* `minClientVersions` are recorded as ignored; `displayName`, `publisher`,
|
|
16
|
+
* `category` and `tags` are marketplace metadata Stigmer neither carries
|
|
17
|
+
* nor warns about.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { JsonObject } from "../documents.js";
|
|
21
|
+
import type { Findings } from "../messages.js";
|
|
22
|
+
import {
|
|
23
|
+
type DialectManifest,
|
|
24
|
+
ignoredFieldComponents,
|
|
25
|
+
readDeclaredPaths,
|
|
26
|
+
readIdentity,
|
|
27
|
+
readMcpSources,
|
|
28
|
+
warnUnknownFields,
|
|
29
|
+
} from "./manifest.js";
|
|
30
|
+
|
|
31
|
+
const KNOWN_FIELDS: ReadonlySet<string> = new Set([
|
|
32
|
+
"name",
|
|
33
|
+
"displayName",
|
|
34
|
+
"description",
|
|
35
|
+
"version",
|
|
36
|
+
"minClientVersions",
|
|
37
|
+
"author",
|
|
38
|
+
"publisher",
|
|
39
|
+
"homepage",
|
|
40
|
+
"repository",
|
|
41
|
+
"license",
|
|
42
|
+
"logo",
|
|
43
|
+
"keywords",
|
|
44
|
+
"category",
|
|
45
|
+
"tags",
|
|
46
|
+
"commands",
|
|
47
|
+
"agents",
|
|
48
|
+
"skills",
|
|
49
|
+
"rules",
|
|
50
|
+
"hooks",
|
|
51
|
+
"variables",
|
|
52
|
+
"mcpServers",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export function readCursorManifest(object: JsonObject, path: string, findings: Findings): DialectManifest {
|
|
56
|
+
warnUnknownFields(object, KNOWN_FIELDS, path, findings);
|
|
57
|
+
const identity = readIdentity(object, path, findings);
|
|
58
|
+
const skillPaths = readDeclaredPaths(object, "skills", path, findings) ?? [];
|
|
59
|
+
const agentPaths = readDeclaredPaths(object, "agents", path, findings);
|
|
60
|
+
const manifest: { -readonly [K in keyof DialectManifest]: DialectManifest[K] } = {
|
|
61
|
+
dialect: "cursor",
|
|
62
|
+
path,
|
|
63
|
+
identity,
|
|
64
|
+
skillPaths,
|
|
65
|
+
mcpConfigs: readMcpSources(object, "mcpServers", path, "cursor", findings),
|
|
66
|
+
ignored: ignoredFieldComponents(object, path),
|
|
67
|
+
};
|
|
68
|
+
if (agentPaths !== undefined) manifest.agentPaths = agentPaths;
|
|
69
|
+
if (object["variables"] !== undefined) {
|
|
70
|
+
manifest.variables = { dialect: "cursor", value: object["variables"], manifest: path };
|
|
71
|
+
}
|
|
72
|
+
return manifest;
|
|
73
|
+
}
|