@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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent files (`agents/*.md`) into the shape `SubAgent` takes.
|
|
3
|
+
*
|
|
4
|
+
* Cursor and Claude Code define this component; the open format does not,
|
|
5
|
+
* so `agents/` is read only when a vendor manifest is present (`detect.ts`
|
|
6
|
+
* decides) and recorded as ignored otherwise. Files come from the default
|
|
7
|
+
* `agents/` directory or, when any manifest declares `agents`, from the
|
|
8
|
+
* declared paths only (Claude's "replaces the default"), each of which may
|
|
9
|
+
* be one `.md` file or a directory of them.
|
|
10
|
+
*
|
|
11
|
+
* The body of the file is the sub-agent's prompt. A body under
|
|
12
|
+
* `SUB_AGENT_INSTRUCTIONS_MIN` characters is refused, because that is the
|
|
13
|
+
* proto's floor for `SubAgent.instructions` and a parser that let it through
|
|
14
|
+
* would add a second silent path after the runner's own (which drops a
|
|
15
|
+
* sub-agent whose model is unregistered). Frontmatter is optional (a bare
|
|
16
|
+
* prompt is named after its file, Claude's rule) but must parse when
|
|
17
|
+
* present: Claude degrades an unparseable header to a file-named agent with
|
|
18
|
+
* every field ignored, and a faithful install is better refused than
|
|
19
|
+
* quietly stripped.
|
|
20
|
+
*
|
|
21
|
+
* `model` is classified into an alias, never a model id (see `ModelHint`).
|
|
22
|
+
* Claude `skills:` names are matched against the plugin's own skills. Every
|
|
23
|
+
* other frontmatter field (`tools`, `disallowedTools`, `effort`, `maxTurns`,
|
|
24
|
+
* `readonly`, `is_background`, `memory`, `isolation`, ...) is a warning,
|
|
25
|
+
* once per field per agent: they configure the IDE's loop, not the
|
|
26
|
+
* sub-agent's prompt.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ManifestSet } from "../detect.js";
|
|
30
|
+
import { readText } from "../documents.js";
|
|
31
|
+
import { basename, comparePaths, joinPath, type PluginFileIndex } from "../files.js";
|
|
32
|
+
import { extractFrontmatter, parseFrontmatter } from "../frontmatter.js";
|
|
33
|
+
import type { Findings } from "../messages.js";
|
|
34
|
+
import type { ModelAlias, ModelHint, PluginSkill, PluginSubAgent } from "../types.js";
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_AGENTS_DIR = "agents";
|
|
37
|
+
|
|
38
|
+
/** The proto's `SubAgent.instructions` `min_len`. */
|
|
39
|
+
export const SUB_AGENT_INSTRUCTIONS_MIN = 10;
|
|
40
|
+
|
|
41
|
+
const READ_FIELDS: ReadonlySet<string> = new Set(["name", "description", "model", "skills"]);
|
|
42
|
+
|
|
43
|
+
export function normaliseSubAgents(
|
|
44
|
+
index: PluginFileIndex,
|
|
45
|
+
set: ManifestSet,
|
|
46
|
+
skills: readonly PluginSkill[],
|
|
47
|
+
findings: Findings,
|
|
48
|
+
): readonly PluginSubAgent[] {
|
|
49
|
+
if (!set.readsAgents) return [];
|
|
50
|
+
const skillNames = new Set(skills.map((skill) => skill.name));
|
|
51
|
+
const agents: PluginSubAgent[] = [];
|
|
52
|
+
const seen = new Set<string>();
|
|
53
|
+
for (const path of discoverAgentFiles(index, set, findings)) {
|
|
54
|
+
const agent = readSubAgent(index, path, skillNames, findings);
|
|
55
|
+
if (agent === undefined) continue;
|
|
56
|
+
if (seen.has(agent.name)) {
|
|
57
|
+
findings.error("sub-agent-name-duplicate", { subject: agent.name, path });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
seen.add(agent.name);
|
|
61
|
+
agents.push(agent);
|
|
62
|
+
}
|
|
63
|
+
return agents;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function discoverAgentFiles(index: PluginFileIndex, set: ManifestSet, findings: Findings): readonly string[] {
|
|
67
|
+
const files = new Set<string>();
|
|
68
|
+
const addMarkdownUnder = (dir: string): void => {
|
|
69
|
+
for (const name of index.childFiles(dir)) {
|
|
70
|
+
if (name.endsWith(".md")) files.add(joinPath(dir, name));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const declared = set.manifests.flatMap((m) => m.agentPaths ?? []);
|
|
75
|
+
if (set.manifests.every((m) => m.agentPaths === undefined)) {
|
|
76
|
+
addMarkdownUnder(DEFAULT_AGENTS_DIR);
|
|
77
|
+
}
|
|
78
|
+
for (const { path, manifest } of declared) {
|
|
79
|
+
if (path !== "" && index.has(path) && path.endsWith(".md")) {
|
|
80
|
+
files.add(path);
|
|
81
|
+
} else if (index.isDirectory(path)) {
|
|
82
|
+
addMarkdownUnder(path);
|
|
83
|
+
} else {
|
|
84
|
+
findings.warn("path-missing", { path: manifest, subject: path === "" ? "." : `./${path}` });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...files].sort(comparePaths);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readSubAgent(
|
|
91
|
+
index: PluginFileIndex,
|
|
92
|
+
path: string,
|
|
93
|
+
skillNames: ReadonlySet<string>,
|
|
94
|
+
findings: Findings,
|
|
95
|
+
): PluginSubAgent | undefined {
|
|
96
|
+
const text = readText(index, path, "subAgent", findings);
|
|
97
|
+
if (text === undefined) return undefined;
|
|
98
|
+
|
|
99
|
+
const extracted = extractFrontmatter(text);
|
|
100
|
+
let fieldsMap: Readonly<Record<string, unknown>> = {};
|
|
101
|
+
let body = text;
|
|
102
|
+
if (extracted.ok) {
|
|
103
|
+
const parsed = parseFrontmatter(extracted.yaml);
|
|
104
|
+
if (!parsed.ok) {
|
|
105
|
+
findings.error("sub-agent-frontmatter-unreadable", { path, detail: parsed.detail });
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
fieldsMap = parsed.fields;
|
|
109
|
+
body = extracted.body;
|
|
110
|
+
} else if (extracted.reason === "unclosed") {
|
|
111
|
+
findings.error("sub-agent-frontmatter-unreadable", { path, detail: "the frontmatter is not closed (missing the closing '---')" });
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stem = basename(path).replace(/\.md$/, "");
|
|
116
|
+
let name: string;
|
|
117
|
+
if (typeof fieldsMap["name"] === "string" && fieldsMap["name"] !== "") {
|
|
118
|
+
name = fieldsMap["name"];
|
|
119
|
+
} else {
|
|
120
|
+
name = stem;
|
|
121
|
+
findings.warn("sub-agent-name-defaulted", { path, subject: name });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const instructions = body.trim();
|
|
125
|
+
if (instructions.length < SUB_AGENT_INSTRUCTIONS_MIN) {
|
|
126
|
+
findings.error("sub-agent-instructions-short", { path, subject: name, detail: String(SUB_AGENT_INSTRUCTIONS_MIN) });
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const description = fieldsMap["description"];
|
|
131
|
+
const requested = fieldsMap["skills"];
|
|
132
|
+
const matchedSkills: string[] = [];
|
|
133
|
+
if (Array.isArray(requested)) {
|
|
134
|
+
for (const skill of requested) {
|
|
135
|
+
if (typeof skill !== "string") continue;
|
|
136
|
+
if (skillNames.has(skill)) matchedSkills.push(skill);
|
|
137
|
+
else findings.warn("sub-agent-skill-unknown", { path, subject: name, detail: skill });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let modelHint: ModelHint | undefined;
|
|
142
|
+
const model = fieldsMap["model"];
|
|
143
|
+
if (typeof model === "string" && model.trim() !== "") {
|
|
144
|
+
modelHint = classifyModel(model);
|
|
145
|
+
if (modelHint.alias === "unknown") findings.warn("sub-agent-model-unknown", { path, subject: name, detail: model });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const field of Object.keys(fieldsMap)) {
|
|
149
|
+
if (!READ_FIELDS.has(field)) findings.warn("sub-agent-field-ignored", { path, subject: name, detail: field });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
name,
|
|
154
|
+
...(typeof description === "string" && description !== "" && { description }),
|
|
155
|
+
instructions,
|
|
156
|
+
skillNames: matchedSkills,
|
|
157
|
+
...(modelHint !== undefined && { modelHint }),
|
|
158
|
+
path,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A dialect's `model` value to an alias. Cursor writes `fast` and
|
|
164
|
+
* `inherit`; Claude writes `sonnet`, `opus`, `haiku`, `inherit` or a full
|
|
165
|
+
* model id, which contains one of those family names. Anything else
|
|
166
|
+
* (Cursor's `grok-4.6[effort=xhigh]`, a vendor id Stigmer does not serve)
|
|
167
|
+
* is `unknown` with the raw text kept for the installer's warning.
|
|
168
|
+
*/
|
|
169
|
+
export function classifyModel(raw: string): ModelHint {
|
|
170
|
+
const value = raw.trim().toLowerCase();
|
|
171
|
+
const alias: ModelAlias =
|
|
172
|
+
value === "inherit"
|
|
173
|
+
? "inherit"
|
|
174
|
+
: value === "fast"
|
|
175
|
+
? "fast"
|
|
176
|
+
: value.includes("haiku")
|
|
177
|
+
? "haiku"
|
|
178
|
+
: value.includes("sonnet")
|
|
179
|
+
? "sonnet"
|
|
180
|
+
: value.includes("opus")
|
|
181
|
+
? "opus"
|
|
182
|
+
: "unknown";
|
|
183
|
+
return { raw, alias };
|
|
184
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable declarations, from the dialects that have them, reconciled with
|
|
3
|
+
* the references the servers make.
|
|
4
|
+
*
|
|
5
|
+
* Cursor declares `variables` as a JSON Schema object with no sensitivity
|
|
6
|
+
* flag, so every Cursor variable is a secret on Stigmer (a token treated as
|
|
7
|
+
* plain text is the costly mistake; a region name treated as a secret is
|
|
8
|
+
* an inconvenience). Claude declares `userConfig` entries with `sensitive`
|
|
9
|
+
* and `required`, plus `default`, `options`, `multiple`, `min`, `max` and
|
|
10
|
+
* the `directory`, `file`, `number`, `boolean` types, none of which a
|
|
11
|
+
* Stigmer variable carries (values are strings supplied by the user, and a
|
|
12
|
+
* local path means nothing in the sandbox); each is warned once.
|
|
13
|
+
*
|
|
14
|
+
* Reconciliation is what makes an imported server start: the runner
|
|
15
|
+
* filters a stdio server's environment to its declared keys and resolves
|
|
16
|
+
* `${VAR}` in headers and arguments strictly, so a variable a server
|
|
17
|
+
* references but no manifest declares would never reach it. Such a variable
|
|
18
|
+
* is declared here as a required secret, with a warning naming the server.
|
|
19
|
+
* A declared variable no server references is warned too; it is not wrong,
|
|
20
|
+
* only useless.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ManifestSet } from "../detect.js";
|
|
24
|
+
import { describeValue, fields, isJsonObject, isStringArray, type JsonObject } from "../documents.js";
|
|
25
|
+
import type { Findings } from "../messages.js";
|
|
26
|
+
import { VARIABLE_NAME_PATTERN } from "../placeholders.js";
|
|
27
|
+
import type { PluginVariable } from "../types.js";
|
|
28
|
+
import type { McpServersResult } from "./mcp-servers.js";
|
|
29
|
+
|
|
30
|
+
export function normaliseVariables(set: ManifestSet, servers: McpServersResult, findings: Findings): readonly PluginVariable[] {
|
|
31
|
+
const declared = new Map<string, PluginVariable>();
|
|
32
|
+
const declaredIn = new Map<string, string>();
|
|
33
|
+
for (const manifest of set.manifests) {
|
|
34
|
+
const declaration = manifest.variables;
|
|
35
|
+
if (declaration === undefined) continue;
|
|
36
|
+
const variables =
|
|
37
|
+
declaration.dialect === "cursor"
|
|
38
|
+
? readCursorVariables(declaration.value, declaration.manifest, findings)
|
|
39
|
+
: readClaudeVariables(declaration.value, declaration.manifest, findings);
|
|
40
|
+
for (const variable of variables) {
|
|
41
|
+
// Two manifests declaring one name: the identity manifest's wins.
|
|
42
|
+
if (declared.has(variable.name)) continue;
|
|
43
|
+
declared.set(variable.name, variable);
|
|
44
|
+
declaredIn.set(variable.name, declaration.manifest);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const referencedBy = new Map<string, string>();
|
|
49
|
+
for (const server of servers.servers) {
|
|
50
|
+
for (const name of server.env) {
|
|
51
|
+
if (!referencedBy.has(name)) referencedBy.set(name, server.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A refused server's references are unknown, so an "unreferenced"
|
|
56
|
+
// warning would be a consequence of the refusal, not advice.
|
|
57
|
+
if (servers.refused === 0) {
|
|
58
|
+
for (const [name, variable] of declared) {
|
|
59
|
+
if (!referencedBy.has(name)) {
|
|
60
|
+
findings.warn("variable-unreferenced", { subject: variable.name, path: declaredIn.get(name) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const inferred: PluginVariable[] = [];
|
|
66
|
+
for (const [name, server] of [...referencedBy].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
67
|
+
if (declared.has(name)) continue;
|
|
68
|
+
findings.warn("variable-inferred", { subject: name, detail: server });
|
|
69
|
+
inferred.push({ name, isSecret: true, optional: false, declaredBy: "inferred" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [...declared.values(), ...inferred];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Cursor: `{ type: "object", properties: { NAME: { type, title, description } }, required: [NAME] }`.
|
|
77
|
+
*/
|
|
78
|
+
function readCursorVariables(value: unknown, manifest: string, findings: Findings): readonly PluginVariable[] {
|
|
79
|
+
if (!isJsonObject(value) || value["type"] !== "object") {
|
|
80
|
+
findings.error("manifest-field-type", { path: manifest, subject: "variables", detail: "a JSON Schema object with type 'object'" });
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const properties = value["properties"] ?? {};
|
|
84
|
+
if (!isJsonObject(properties)) {
|
|
85
|
+
findings.error("manifest-field-type", { path: manifest, subject: "variables.properties", detail: "an object" });
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const required = value["required"] ?? [];
|
|
89
|
+
if (!isStringArray(required)) {
|
|
90
|
+
findings.error("manifest-field-type", { path: manifest, subject: "variables.required", detail: "an array of strings" });
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const variables: PluginVariable[] = [];
|
|
95
|
+
for (const [name, schema] of fields(properties)) {
|
|
96
|
+
if (!VARIABLE_NAME_PATTERN.test(name)) {
|
|
97
|
+
findings.error("variable-name-invalid", { subject: name, path: manifest });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const property = isJsonObject(schema) ? schema : {};
|
|
101
|
+
const type = property["type"];
|
|
102
|
+
if (type !== undefined && type !== "string") {
|
|
103
|
+
findings.warn("variable-type-narrowed", { subject: name, path: manifest, detail: describeValue(type) });
|
|
104
|
+
}
|
|
105
|
+
if (property["default"] !== undefined) findings.warn("variable-default-dropped", { subject: name, path: manifest });
|
|
106
|
+
if (property["enum"] !== undefined) findings.warn("variable-option-dropped", { subject: name, path: manifest, detail: "enum" });
|
|
107
|
+
const description = describe(property);
|
|
108
|
+
variables.push({
|
|
109
|
+
name,
|
|
110
|
+
...(description !== undefined && { description }),
|
|
111
|
+
isSecret: true,
|
|
112
|
+
optional: !required.includes(name),
|
|
113
|
+
declaredBy: "cursor",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return variables;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Claude: `{ KEY: { type, title, description, sensitive, required, default, options, multiple, min, max } }`.
|
|
121
|
+
*/
|
|
122
|
+
function readClaudeVariables(value: unknown, manifest: string, findings: Findings): readonly PluginVariable[] {
|
|
123
|
+
if (!isJsonObject(value)) {
|
|
124
|
+
findings.error("manifest-field-type", { path: manifest, subject: "userConfig", detail: "an object" });
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const variables: PluginVariable[] = [];
|
|
128
|
+
for (const [name, entry] of fields(value)) {
|
|
129
|
+
if (!VARIABLE_NAME_PATTERN.test(name)) {
|
|
130
|
+
findings.error("variable-name-invalid", { subject: name, path: manifest });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!isJsonObject(entry)) {
|
|
134
|
+
findings.error("manifest-field-type", { path: manifest, subject: `userConfig.${name}`, detail: "an object" });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const type = entry["type"];
|
|
138
|
+
if (type !== undefined && type !== "string") {
|
|
139
|
+
findings.warn("variable-type-narrowed", { subject: name, path: manifest, detail: describeValue(type) });
|
|
140
|
+
}
|
|
141
|
+
if (entry["default"] !== undefined) findings.warn("variable-default-dropped", { subject: name, path: manifest });
|
|
142
|
+
for (const option of ["options", "multiple", "min", "max"]) {
|
|
143
|
+
if (entry[option] !== undefined) findings.warn("variable-option-dropped", { subject: name, path: manifest, detail: option });
|
|
144
|
+
}
|
|
145
|
+
const description = describe(entry);
|
|
146
|
+
variables.push({
|
|
147
|
+
name,
|
|
148
|
+
...(description !== undefined && { description }),
|
|
149
|
+
isSecret: entry["sensitive"] === true,
|
|
150
|
+
optional: entry["required"] !== true,
|
|
151
|
+
declaredBy: "claude",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return variables;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `title` and `description` joined, whichever are present. */
|
|
158
|
+
function describe(entry: JsonObject): string | undefined {
|
|
159
|
+
const parts = [entry["title"], entry["description"]].filter((part): part is string => typeof part === "string" && part !== "");
|
|
160
|
+
return parts.length === 0 ? undefined : parts.join(": ");
|
|
161
|
+
}
|
package/src/outcome.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outcome of reading a package and the closed vocabulary of findings.
|
|
3
|
+
*
|
|
4
|
+
* The shape is the server's `ConnectRunOutcome`: a discriminated union on
|
|
5
|
+
* `ok`, with a closed `kind` union behind the failure arm, so a consumer
|
|
6
|
+
* switches on kinds and the compiler closes the switch. An error refuses the
|
|
7
|
+
* package; a warning never does. Both are `PluginFinding`s with the same
|
|
8
|
+
* fields, so one renderer prints both.
|
|
9
|
+
*
|
|
10
|
+
* Two error postures meet here and the header of each module states which
|
|
11
|
+
* applies. Where the open format tells a client to skip a broken component
|
|
12
|
+
* and keep loading, Stigmer refuses the package: an installed agent that
|
|
13
|
+
* quietly lacks a server or a sub-agent is worse than a refused install.
|
|
14
|
+
* But the reader keeps scanning past a fatal finding so one run reports
|
|
15
|
+
* every problem and an author fixes them in one pass.
|
|
16
|
+
*
|
|
17
|
+
* Every kind has exactly one sentence, in `messages.ts`, and the adversarial
|
|
18
|
+
* suite has exactly one fixture per kind. Adding a kind means adding both.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PluginPackage } from "./types.js";
|
|
22
|
+
|
|
23
|
+
export type PluginErrorKind =
|
|
24
|
+
// Manifests
|
|
25
|
+
| "no-manifest"
|
|
26
|
+
| "manifest-unreadable"
|
|
27
|
+
| "manifest-schema-missing"
|
|
28
|
+
| "manifest-schema-unsupported"
|
|
29
|
+
| "manifest-name-missing"
|
|
30
|
+
| "manifest-name-invalid"
|
|
31
|
+
| "manifest-name-conflict"
|
|
32
|
+
| "manifest-field-type"
|
|
33
|
+
// Paths
|
|
34
|
+
| "path-not-relative"
|
|
35
|
+
| "path-escapes-root"
|
|
36
|
+
| "path-glob-unsupported"
|
|
37
|
+
| "path-uncontained"
|
|
38
|
+
| "document-too-large"
|
|
39
|
+
// Skills
|
|
40
|
+
| "skill-frontmatter-missing"
|
|
41
|
+
| "skill-frontmatter-unclosed"
|
|
42
|
+
| "skill-frontmatter-unreadable"
|
|
43
|
+
| "skill-name-invalid"
|
|
44
|
+
| "skill-name-duplicate"
|
|
45
|
+
// MCP configuration files
|
|
46
|
+
| "mcp-config-unreadable"
|
|
47
|
+
| "mcp-config-shape"
|
|
48
|
+
| "mcp-config-schema-missing"
|
|
49
|
+
| "mcp-config-schema-unsupported"
|
|
50
|
+
| "mcp-config-field-unknown"
|
|
51
|
+
// MCP server entries
|
|
52
|
+
| "mcp-server-shape"
|
|
53
|
+
| "mcp-server-type-missing"
|
|
54
|
+
| "mcp-server-transport-unknown"
|
|
55
|
+
| "mcp-server-type-ambiguous"
|
|
56
|
+
| "mcp-server-type-unknown"
|
|
57
|
+
| "mcp-server-field-unknown"
|
|
58
|
+
| "mcp-server-field-type"
|
|
59
|
+
| "mcp-server-url-missing"
|
|
60
|
+
| "mcp-server-url-invalid"
|
|
61
|
+
| "mcp-server-url-variable"
|
|
62
|
+
| "mcp-server-command-missing"
|
|
63
|
+
| "mcp-server-command-invalid"
|
|
64
|
+
| "mcp-server-command-relative"
|
|
65
|
+
| "mcp-server-plugin-root-reference"
|
|
66
|
+
| "mcp-server-cwd-unsupported"
|
|
67
|
+
| "mcp-server-env-literal"
|
|
68
|
+
| "mcp-server-env-rename"
|
|
69
|
+
| "mcp-server-name-duplicate"
|
|
70
|
+
| "mcp-server-header-duplicate"
|
|
71
|
+
| "mcp-server-header-invalid"
|
|
72
|
+
// Sub-agents
|
|
73
|
+
| "sub-agent-frontmatter-unreadable"
|
|
74
|
+
| "sub-agent-instructions-short"
|
|
75
|
+
| "sub-agent-name-duplicate"
|
|
76
|
+
// Variables
|
|
77
|
+
| "variable-name-invalid"
|
|
78
|
+
// The ai.stigmer/ overlay
|
|
79
|
+
| "overlay-server-unknown"
|
|
80
|
+
| "overlay-document-unknown";
|
|
81
|
+
|
|
82
|
+
export type PluginWarningKind =
|
|
83
|
+
| "manifest-field-unknown"
|
|
84
|
+
| "manifest-extensions-invalid"
|
|
85
|
+
| "path-missing"
|
|
86
|
+
| "skill-name-defaulted"
|
|
87
|
+
| "skill-name-differs-from-directory"
|
|
88
|
+
| "skill-description-missing"
|
|
89
|
+
| "mcp-config-field-ignored"
|
|
90
|
+
| "mcp-server-sse-mapped"
|
|
91
|
+
| "mcp-server-field-ignored"
|
|
92
|
+
| "mcp-server-auth-ignored"
|
|
93
|
+
| "variable-inferred"
|
|
94
|
+
| "variable-unreferenced"
|
|
95
|
+
| "variable-default-dropped"
|
|
96
|
+
| "variable-type-narrowed"
|
|
97
|
+
| "variable-option-dropped"
|
|
98
|
+
| "sub-agent-name-defaulted"
|
|
99
|
+
| "sub-agent-skill-unknown"
|
|
100
|
+
| "sub-agent-model-unknown"
|
|
101
|
+
| "sub-agent-field-ignored";
|
|
102
|
+
|
|
103
|
+
export type PluginFindingKind = PluginErrorKind | PluginWarningKind;
|
|
104
|
+
|
|
105
|
+
export interface PluginFinding {
|
|
106
|
+
readonly kind: PluginFindingKind;
|
|
107
|
+
/** The file, or the declared path, the finding is about. */
|
|
108
|
+
readonly path?: string;
|
|
109
|
+
/** The skill, server, sub-agent, variable or field the finding names. */
|
|
110
|
+
readonly subject?: string;
|
|
111
|
+
/** A second identifier the sentence needs (a field name, a parser's own message). */
|
|
112
|
+
readonly detail?: string;
|
|
113
|
+
/** The one sentence for this kind, from `messages.ts`. */
|
|
114
|
+
readonly message: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What `read` needs to compose a finding: everything but the sentence. */
|
|
118
|
+
export type FindingContext = Pick<PluginFinding, "path" | "subject" | "detail">;
|
|
119
|
+
|
|
120
|
+
export type PluginReadOutcome =
|
|
121
|
+
| { readonly ok: true; readonly plugin: PluginPackage; readonly warnings: readonly PluginFinding[] }
|
|
122
|
+
| { readonly ok: false; readonly errors: readonly PluginFinding[]; readonly warnings: readonly PluginFinding[] };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `${VAR}` vocabulary: what counts as a variable reference, what counts
|
|
3
|
+
* as a reference to the plugin's own files, and the Claude rewrite.
|
|
4
|
+
*
|
|
5
|
+
* `PLACEHOLDER_PATTERN` is the runner's own scanner, verbatim: the runner
|
|
6
|
+
* resolves `${VAR}` strictly in a server's headers and arguments and refuses
|
|
7
|
+
* to start a server with an unresolved one, so this is the definition of "a
|
|
8
|
+
* reference" in every dialect, including the open format whose text forbids
|
|
9
|
+
* expansion in headers. A plugin's `${API_KEY}` in a header means "the
|
|
10
|
+
* caller's API_KEY" on Stigmer whichever tool wrote it.
|
|
11
|
+
*
|
|
12
|
+
* Claude's `${user_config.KEY}` carries a dot the runner's scanner would
|
|
13
|
+
* never match, so it is rewritten to `${KEY}` before anything reads it; the
|
|
14
|
+
* rewrite is what makes a Claude plugin's server start, not a courtesy.
|
|
15
|
+
*
|
|
16
|
+
* The plugin-root placeholders are the one class the library refuses: the
|
|
17
|
+
* runner mounts nothing from a plugin, so a server that reaches for
|
|
18
|
+
* `${PLUGIN_ROOT}` (any dialect's spelling) can never find what it points
|
|
19
|
+
* at. The set is checked before the generic scan because `${PLUGIN_ROOT}`
|
|
20
|
+
* also matches the generic pattern.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The runner's `${VAR_NAME}` scanner (`shared/placeholder-resolver.ts`). */
|
|
24
|
+
export const PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
25
|
+
|
|
26
|
+
/** A variable name the runner's scanner can reference. */
|
|
27
|
+
export const VARIABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
28
|
+
|
|
29
|
+
const USER_CONFIG_PATTERN = /\$\{user_config\.([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Every spelling of "the plugin's own directory" across the dialects:
|
|
33
|
+
* the open format's, Claude Code's (plus its project directory), Cursor's.
|
|
34
|
+
*/
|
|
35
|
+
export const PLUGIN_ROOT_PLACEHOLDERS: ReadonlySet<string> = new Set([
|
|
36
|
+
"${PLUGIN_ROOT}",
|
|
37
|
+
"${PLUGIN_DATA}",
|
|
38
|
+
"${CLAUDE_PLUGIN_ROOT}",
|
|
39
|
+
"${CLAUDE_PLUGIN_DATA}",
|
|
40
|
+
"${CLAUDE_PROJECT_DIR}",
|
|
41
|
+
"${CURSOR_PLUGIN_ROOT}",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** The reserved environment names the open format forbids a server's `env` to set. */
|
|
45
|
+
export const PLUGIN_ROOT_ENV_NAMES: ReadonlySet<string> = new Set(["PLUGIN_ROOT", "PLUGIN_DATA"]);
|
|
46
|
+
|
|
47
|
+
/** `${user_config.KEY}` -> `${KEY}`. */
|
|
48
|
+
export function rewriteUserConfig(value: string): string {
|
|
49
|
+
return value.replace(USER_CONFIG_PATTERN, (_match, key: string) => `\${${key}}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The first plugin-root placeholder in `value`, or `undefined`. */
|
|
53
|
+
export function findPluginRootPlaceholder(value: string): string | undefined {
|
|
54
|
+
for (const placeholder of PLUGIN_ROOT_PLACEHOLDERS) {
|
|
55
|
+
if (value.includes(placeholder)) return placeholder;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The variable names referenced in `value`, in order of appearance, deduplicated. */
|
|
61
|
+
export function referencedVariables(value: string): readonly string[] {
|
|
62
|
+
const names: string[] = [];
|
|
63
|
+
for (const match of value.matchAll(PLACEHOLDER_PATTERN)) {
|
|
64
|
+
const name = match[1];
|
|
65
|
+
if (name !== undefined && !names.includes(name)) names.push(name);
|
|
66
|
+
}
|
|
67
|
+
return names;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The variable name when `value` is exactly one placeholder and nothing else; otherwise `undefined`. */
|
|
71
|
+
export function singlePlaceholderName(value: string): string | undefined {
|
|
72
|
+
const match = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(value);
|
|
73
|
+
return match?.[1];
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one entry: a `PluginFiles` in, a `PluginReadOutcome` out.
|
|
3
|
+
*
|
|
4
|
+
* The read is a fixed sequence, each stage a pure function over the index
|
|
5
|
+
* and the stages before it: containment of the listed paths; manifest
|
|
6
|
+
* detection and identity; skills; MCP servers; variables (which need the
|
|
7
|
+
* servers' references); sub-agents (which need the skills' names); the
|
|
8
|
+
* `ai.stigmer/` overlay (which needs the servers' names); the ignored
|
|
9
|
+
* components. Every stage records findings and keeps going, so the outcome
|
|
10
|
+
* names every problem in the package at once, and the plugin is returned
|
|
11
|
+
* only when no stage refused.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here throws on a plugin's content. A throw means a reader broke
|
|
14
|
+
* its contract (a listed path that cannot be read) or the library has a bug.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { detectManifests } from "./detect.js";
|
|
18
|
+
import { isContainedPath, PluginFileIndex, type PluginFiles } from "./files.js";
|
|
19
|
+
import { Findings } from "./messages.js";
|
|
20
|
+
import { dedupeIgnored, ignoredOnDisk } from "./normalise/ignored.js";
|
|
21
|
+
import { normaliseMcpServers } from "./normalise/mcp-servers.js";
|
|
22
|
+
import { normaliseOverlay } from "./normalise/overlay.js";
|
|
23
|
+
import { normaliseSkills } from "./normalise/skills.js";
|
|
24
|
+
import { normaliseSubAgents } from "./normalise/sub-agents.js";
|
|
25
|
+
import { normaliseVariables } from "./normalise/variables.js";
|
|
26
|
+
import type { PluginReadOutcome } from "./outcome.js";
|
|
27
|
+
import type { PluginPackage } from "./types.js";
|
|
28
|
+
|
|
29
|
+
export function readPluginPackage(files: PluginFiles): PluginReadOutcome {
|
|
30
|
+
const findings = new Findings();
|
|
31
|
+
|
|
32
|
+
for (const entry of files.entries) {
|
|
33
|
+
if (!isContainedPath(entry.path)) findings.error("path-uncontained", { path: entry.path });
|
|
34
|
+
}
|
|
35
|
+
const index = new PluginFileIndex(files);
|
|
36
|
+
|
|
37
|
+
const set = detectManifests(index, findings);
|
|
38
|
+
if (set === undefined) {
|
|
39
|
+
return { ok: false, errors: findings.errors, warnings: findings.warnings };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skills = normaliseSkills(index, set, findings);
|
|
43
|
+
const servers = normaliseMcpServers(index, set.mcpSources, findings);
|
|
44
|
+
const mcpServers = servers.servers;
|
|
45
|
+
const variables = normaliseVariables(set, servers, findings);
|
|
46
|
+
const subAgents = normaliseSubAgents(index, set, skills, findings);
|
|
47
|
+
const overlay = normaliseOverlay(index, servers.declaredNames, findings);
|
|
48
|
+
const ignored = dedupeIgnored([ignoredOnDisk(index, set.readsAgents), ...set.manifests.map((m) => m.ignored)]);
|
|
49
|
+
|
|
50
|
+
if (findings.errors.length > 0 || set.name === undefined) {
|
|
51
|
+
return { ok: false, errors: findings.errors, warnings: findings.warnings };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const identity = set.manifests[0]?.identity ?? {};
|
|
55
|
+
const plugin: PluginPackage = {
|
|
56
|
+
name: set.name,
|
|
57
|
+
...(identity.version !== undefined && { version: identity.version }),
|
|
58
|
+
...(identity.description !== undefined && { description: identity.description }),
|
|
59
|
+
...(identity.author !== undefined && { author: identity.author }),
|
|
60
|
+
...(identity.homepage !== undefined && { homepage: identity.homepage }),
|
|
61
|
+
...(identity.repository !== undefined && { repository: identity.repository }),
|
|
62
|
+
...(identity.license !== undefined && { license: identity.license }),
|
|
63
|
+
keywords: identity.keywords ?? [],
|
|
64
|
+
dialect: set.dialect,
|
|
65
|
+
manifestsFound: set.manifests.map((m) => m.path),
|
|
66
|
+
skills,
|
|
67
|
+
mcpServers,
|
|
68
|
+
subAgents,
|
|
69
|
+
variables,
|
|
70
|
+
overlay,
|
|
71
|
+
ignored,
|
|
72
|
+
};
|
|
73
|
+
return { ok: true, plugin, warnings: findings.warnings };
|
|
74
|
+
}
|