@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,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML frontmatter extraction for `SKILL.md` and sub-agent files, and the
|
|
3
|
+
* skill name rule.
|
|
4
|
+
*
|
|
5
|
+
* The delimiter discipline is the server's skill push gate's: the file must
|
|
6
|
+
* open with `---` on its first line and close with `---` on a line of its
|
|
7
|
+
* own, both compared after trimming whitespace. Extraction and parsing are
|
|
8
|
+
* separate steps because the two callers differ on what a missing block
|
|
9
|
+
* means: a `SKILL.md` without frontmatter is refused (the Agent Skills
|
|
10
|
+
* format requires it), while a sub-agent file without frontmatter is a
|
|
11
|
+
* prompt named after its file (the Claude Code posture).
|
|
12
|
+
*
|
|
13
|
+
* `SKILL_NAME_PATTERN` is the server's rule for a skill name (kebab-case,
|
|
14
|
+
* optionally dot-scoped), a strict superset of the Agent Skills rule
|
|
15
|
+
* (lowercase alphanumerics and hyphens), so no skill valid under the open
|
|
16
|
+
* format is refused here. This module is meant to become the pattern's one
|
|
17
|
+
* home; the server's `frontmatter.ts` carries the same regex today.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { parse as parseYaml } from "yaml";
|
|
21
|
+
|
|
22
|
+
import { isJsonObject, type JsonObject } from "./documents.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Kebab-case, optionally scoped with dot-separated namespaces. Every
|
|
26
|
+
* segment between separators is alphanumeric, so a name cannot start or end
|
|
27
|
+
* with a separator or contain consecutive separators. The derived slug
|
|
28
|
+
* renders dots as hyphens.
|
|
29
|
+
*/
|
|
30
|
+
export const SKILL_NAME_PATTERN = /^[a-z0-9]+([.-][a-z0-9]+)*$/;
|
|
31
|
+
|
|
32
|
+
export type FrontmatterExtraction =
|
|
33
|
+
| { readonly ok: true; readonly yaml: string; readonly body: string }
|
|
34
|
+
| { readonly ok: false; readonly reason: "missing" | "unclosed" };
|
|
35
|
+
|
|
36
|
+
/** Split a document into its frontmatter YAML and its body. */
|
|
37
|
+
export function extractFrontmatter(content: string): FrontmatterExtraction {
|
|
38
|
+
const lines = content.split(/\r?\n/);
|
|
39
|
+
if (lines.length === 0 || lines[0]?.trim() !== "---") {
|
|
40
|
+
return { ok: false, reason: "missing" };
|
|
41
|
+
}
|
|
42
|
+
for (let i = 1; i < lines.length; i++) {
|
|
43
|
+
if (lines[i]?.trim() === "---") {
|
|
44
|
+
return { ok: true, yaml: lines.slice(1, i).join("\n"), body: lines.slice(i + 1).join("\n") };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { ok: false, reason: "unclosed" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type FrontmatterParse =
|
|
51
|
+
| { readonly ok: true; readonly fields: JsonObject }
|
|
52
|
+
| { readonly ok: false; readonly detail: string };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse frontmatter YAML into a field map. An empty block is an empty map
|
|
56
|
+
* (the caller decides whether required fields are missing); a scalar or a
|
|
57
|
+
* list where a map was expected is a parse failure with its own detail.
|
|
58
|
+
* `yaml`'s defaults are the safe ones: no custom tags, aliases capped.
|
|
59
|
+
*/
|
|
60
|
+
export function parseFrontmatter(yaml: string): FrontmatterParse {
|
|
61
|
+
let value: unknown;
|
|
62
|
+
try {
|
|
63
|
+
value = parseYaml(yaml);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
66
|
+
}
|
|
67
|
+
if (value === null || value === undefined) return { ok: true, fields: {} };
|
|
68
|
+
if (!isJsonObject(value)) return { ok: false, detail: "the frontmatter is not a mapping" };
|
|
69
|
+
return { ok: true, fields: value };
|
|
70
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/plugin-package`: the public surface.
|
|
3
|
+
*
|
|
4
|
+
* `readPluginPackage` is the entry; everything else here is what a consumer
|
|
5
|
+
* needs to supply its input (`PluginFiles`, the caps), route a directory
|
|
6
|
+
* (`MANIFEST_LOCATIONS`, `hasPluginManifest`), or render the outcome (the
|
|
7
|
+
* finding kinds and `isErrorKind`). The dialect readers and normalisers are
|
|
8
|
+
* internal: a consumer never composes a partial read.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { hasPluginManifest, isValidPluginName } from "./detect.js";
|
|
12
|
+
export {
|
|
13
|
+
comparePaths,
|
|
14
|
+
inMemoryPluginFiles,
|
|
15
|
+
isContainedPath,
|
|
16
|
+
PLUGIN_DOCUMENT_LIMITS,
|
|
17
|
+
resolveDeclaredPath,
|
|
18
|
+
type DeclaredPathOutcome,
|
|
19
|
+
type PluginDocumentClass,
|
|
20
|
+
type PluginFileEntry,
|
|
21
|
+
type PluginFiles,
|
|
22
|
+
} from "./files.js";
|
|
23
|
+
export { SKILL_NAME_PATTERN } from "./frontmatter.js";
|
|
24
|
+
export {
|
|
25
|
+
AGENT_PLUGINS_MANIFEST_SCHEMA,
|
|
26
|
+
AGENT_PLUGINS_MCP_SCHEMA,
|
|
27
|
+
errorMessage,
|
|
28
|
+
isErrorKind,
|
|
29
|
+
MANIFEST_LOCATIONS,
|
|
30
|
+
warningMessage,
|
|
31
|
+
} from "./messages.js";
|
|
32
|
+
export { SUB_AGENT_INSTRUCTIONS_MIN, classifyModel } from "./normalise/sub-agents.js";
|
|
33
|
+
export { PLACEHOLDER_PATTERN, VARIABLE_NAME_PATTERN } from "./placeholders.js";
|
|
34
|
+
export type {
|
|
35
|
+
FindingContext,
|
|
36
|
+
PluginErrorKind,
|
|
37
|
+
PluginFinding,
|
|
38
|
+
PluginFindingKind,
|
|
39
|
+
PluginReadOutcome,
|
|
40
|
+
PluginWarningKind,
|
|
41
|
+
} from "./outcome.js";
|
|
42
|
+
export { readPluginPackage } from "./read-plugin-package.js";
|
|
43
|
+
export type {
|
|
44
|
+
IgnoredComponent,
|
|
45
|
+
IgnoredComponentKind,
|
|
46
|
+
ModelAlias,
|
|
47
|
+
ModelHint,
|
|
48
|
+
OverlayDocument,
|
|
49
|
+
OverlayNamedDocument,
|
|
50
|
+
OverlayServerDocument,
|
|
51
|
+
PluginAuthor,
|
|
52
|
+
PluginDialect,
|
|
53
|
+
PluginMcpServer,
|
|
54
|
+
PluginPackage,
|
|
55
|
+
PluginSkill,
|
|
56
|
+
PluginSubAgent,
|
|
57
|
+
PluginVariable,
|
|
58
|
+
StigmerOverlay,
|
|
59
|
+
} from "./types.js";
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one sentence for every finding kind, and the collector that composes
|
|
3
|
+
* findings from a kind and its context.
|
|
4
|
+
*
|
|
5
|
+
* Copy lives here and nowhere else: the CLI and the server print these
|
|
6
|
+
* sentences verbatim, so a plugin refused offline is refused by the server
|
|
7
|
+
* with the same words, and a consumer never composes refusal copy of its
|
|
8
|
+
* own. Every sentence says what is wrong and, where the fix is not obvious,
|
|
9
|
+
* what Stigmer needs instead. Identifiers are single-quoted (the ts-server
|
|
10
|
+
* quoting rule for new copy). The `Record` types are exhaustive over the
|
|
11
|
+
* kind unions, so a kind without a sentence does not compile.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
FindingContext,
|
|
16
|
+
PluginErrorKind,
|
|
17
|
+
PluginFinding,
|
|
18
|
+
PluginFindingKind,
|
|
19
|
+
PluginWarningKind,
|
|
20
|
+
} from "./outcome.js";
|
|
21
|
+
|
|
22
|
+
/** The canonical `$schema` identifiers the open format pins per version. */
|
|
23
|
+
export const AGENT_PLUGINS_MANIFEST_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
24
|
+
export const AGENT_PLUGINS_MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
25
|
+
|
|
26
|
+
/** The four manifest locations, in the precedence the reader applies when there is no root manifest. */
|
|
27
|
+
export const MANIFEST_LOCATIONS = {
|
|
28
|
+
"agent-plugins": "plugin.json",
|
|
29
|
+
claude: ".claude-plugin/plugin.json",
|
|
30
|
+
cursor: ".cursor-plugin/plugin.json",
|
|
31
|
+
codex: ".codex-plugin/plugin.json",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const q = (value: string | undefined): string => `'${value ?? ""}'`;
|
|
35
|
+
const at = (path: string | undefined): string => (path === undefined ? "" : ` in ${q(path)}`);
|
|
36
|
+
|
|
37
|
+
type Sentence = (ctx: FindingContext) => string;
|
|
38
|
+
|
|
39
|
+
const ERROR_MESSAGES: Record<PluginErrorKind, Sentence> = {
|
|
40
|
+
"no-manifest": () =>
|
|
41
|
+
`no plugin manifest found: expected one of ${Object.values(MANIFEST_LOCATIONS)
|
|
42
|
+
.map((p) => q(p))
|
|
43
|
+
.join(", ")}`,
|
|
44
|
+
"manifest-unreadable": (c) => `manifest ${q(c.path)} is not valid JSON: ${c.detail ?? "parse error"}`,
|
|
45
|
+
"manifest-schema-missing": (c) =>
|
|
46
|
+
`manifest ${q(c.path)} is missing the required '$schema' field; an Agent Plugins manifest declares ${q(AGENT_PLUGINS_MANIFEST_SCHEMA)}`,
|
|
47
|
+
"manifest-schema-unsupported": (c) =>
|
|
48
|
+
`manifest ${q(c.path)} declares an unsupported '$schema' ${q(c.detail)}; this reader supports ${q(AGENT_PLUGINS_MANIFEST_SCHEMA)}`,
|
|
49
|
+
"manifest-name-missing": (c) => `manifest ${q(c.path)} is missing the required 'name' field`,
|
|
50
|
+
"manifest-name-invalid": (c) =>
|
|
51
|
+
`plugin name ${q(c.subject)}${at(c.path)} is invalid: 1 to 64 characters of lowercase letters, digits, hyphens and periods, starting and ending alphanumeric, with no '--' or '..'`,
|
|
52
|
+
"manifest-name-conflict": (c) =>
|
|
53
|
+
`manifests disagree on the plugin name: ${q(c.subject)} in ${q(c.path)} versus ${c.detail ?? "another manifest"}`,
|
|
54
|
+
"manifest-field-type": (c) => `field ${q(c.subject)}${at(c.path)} has the wrong type: expected ${c.detail ?? "a different type"}`,
|
|
55
|
+
|
|
56
|
+
"path-not-relative": (c) =>
|
|
57
|
+
`path ${q(c.subject)}${at(c.path)} must be plugin-relative and begin with './'`,
|
|
58
|
+
"path-escapes-root": (c) => `path ${q(c.subject)}${at(c.path)} escapes the plugin root`,
|
|
59
|
+
"path-glob-unsupported": (c) =>
|
|
60
|
+
`path ${q(c.subject)}${at(c.path)} is a glob pattern; declare directories and files by path`,
|
|
61
|
+
"path-uncontained": (c) => `the reader listed a path outside the plugin root: ${q(c.path)}`,
|
|
62
|
+
"document-too-large": (c) =>
|
|
63
|
+
`${q(c.path)} is ${c.subject ?? "too many"} bytes, over the ${c.detail ?? ""}-byte limit for this kind of document`,
|
|
64
|
+
|
|
65
|
+
"skill-frontmatter-missing": (c) => `${q(c.path)} must start with YAML frontmatter ('---')`,
|
|
66
|
+
"skill-frontmatter-unclosed": (c) => `${q(c.path)} frontmatter is not closed (missing the closing '---')`,
|
|
67
|
+
"skill-frontmatter-unreadable": (c) => `${q(c.path)} frontmatter is not valid YAML: ${c.detail ?? "parse error"}`,
|
|
68
|
+
"skill-name-invalid": (c) =>
|
|
69
|
+
`skill name ${q(c.subject)}${at(c.path)} is invalid: lowercase letters, digits and hyphens, optionally dot-scoped, with every segment alphanumeric`,
|
|
70
|
+
"skill-name-duplicate": (c) => `skill name ${q(c.subject)} appears more than once (again${at(c.path)})`,
|
|
71
|
+
|
|
72
|
+
"mcp-config-unreadable": (c) => `MCP configuration ${q(c.path)} is not valid JSON: ${c.detail ?? "parse error"}`,
|
|
73
|
+
"mcp-config-shape": (c) => `MCP configuration ${q(c.path)} must be an object with an 'mcpServers' object`,
|
|
74
|
+
"mcp-config-schema-missing": (c) =>
|
|
75
|
+
`MCP configuration ${q(c.path)} is missing the required '$schema' field; an Agent Plugins configuration declares ${q(AGENT_PLUGINS_MCP_SCHEMA)}`,
|
|
76
|
+
"mcp-config-schema-unsupported": (c) =>
|
|
77
|
+
`MCP configuration ${q(c.path)} declares an unsupported '$schema' ${q(c.detail)}; this reader supports ${q(AGENT_PLUGINS_MCP_SCHEMA)}`,
|
|
78
|
+
"mcp-config-field-unknown": (c) =>
|
|
79
|
+
`MCP configuration ${q(c.path)} has an unexpected top-level field ${q(c.subject)}; only '$schema' and 'mcpServers' are allowed`,
|
|
80
|
+
|
|
81
|
+
"mcp-server-shape": (c) => `MCP server ${q(c.subject)}${at(c.path)} must be an object`,
|
|
82
|
+
"mcp-server-type-missing": (c) =>
|
|
83
|
+
`MCP server ${q(c.subject)}${at(c.path)} is missing the 'type' field the Agent Plugins format requires ('stdio', 'streamable-http' or 'sse')`,
|
|
84
|
+
"mcp-server-transport-unknown": (c) =>
|
|
85
|
+
`MCP server ${q(c.subject)}${at(c.path)} has no 'type' and neither a 'command' nor a 'url' to infer it from`,
|
|
86
|
+
"mcp-server-type-ambiguous": (c) =>
|
|
87
|
+
`MCP server ${q(c.subject)}${at(c.path)} declares both 'command' and 'url'; a server is either stdio or HTTP`,
|
|
88
|
+
"mcp-server-type-unknown": (c) =>
|
|
89
|
+
`MCP server ${q(c.subject)}${at(c.path)} has an unknown 'type' ${q(c.detail)}; expected 'stdio', 'http', 'streamable-http' or 'sse'`,
|
|
90
|
+
"mcp-server-field-unknown": (c) =>
|
|
91
|
+
`MCP server ${q(c.subject)}${at(c.path)} has a field ${q(c.detail)} that its transport does not define`,
|
|
92
|
+
"mcp-server-field-type": (c) => `MCP server ${q(c.subject)}${at(c.path)} field ${q(c.detail)} has the wrong type`,
|
|
93
|
+
"mcp-server-url-missing": (c) => `MCP server ${q(c.subject)}${at(c.path)} is missing the 'url' its HTTP transport requires`,
|
|
94
|
+
"mcp-server-url-invalid": (c) =>
|
|
95
|
+
`MCP server ${q(c.subject)}${at(c.path)} has an invalid 'url' ${q(c.detail)}: expected an absolute HTTPS URL (HTTP only for localhost) with no user information or fragment`,
|
|
96
|
+
"mcp-server-url-variable": (c) =>
|
|
97
|
+
`MCP server ${q(c.subject)}${at(c.path)} has a variable in its 'url'; Stigmer sends the URL as written, so write the URL out and put variables in 'headers'`,
|
|
98
|
+
"mcp-server-command-missing": (c) =>
|
|
99
|
+
`MCP server ${q(c.subject)}${at(c.path)} is missing the 'command' its stdio transport requires`,
|
|
100
|
+
"mcp-server-command-invalid": (c) =>
|
|
101
|
+
`MCP server ${q(c.subject)}${at(c.path)} has a 'command' that is not a single executable name; put arguments in 'args'`,
|
|
102
|
+
"mcp-server-command-relative": (c) =>
|
|
103
|
+
`MCP server ${q(c.subject)}${at(c.path)} runs a command bundled in the plugin ${q(c.detail)}; Stigmer runs only commands on the runner's PATH (for example 'npx' or 'uvx')`,
|
|
104
|
+
"mcp-server-plugin-root-reference": (c) =>
|
|
105
|
+
`MCP server ${q(c.subject)}${at(c.path)} references the plugin's own files through ${q(c.detail)}; Stigmer does not mount plugin files into the runner`,
|
|
106
|
+
"mcp-server-cwd-unsupported": (c) =>
|
|
107
|
+
`MCP server ${q(c.subject)}${at(c.path)} sets a working directory; Stigmer does not mount plugin files, so a bundled directory cannot be reached`,
|
|
108
|
+
"mcp-server-env-literal": (c) =>
|
|
109
|
+
`MCP server ${q(c.subject)}${at(c.path)} sets environment variable ${q(c.detail)} to a literal value; Stigmer passes declared variables by name, so write ${q(`${c.detail ?? "KEY"}: "\${${c.detail ?? "KEY"}}"`)} and declare the variable`,
|
|
110
|
+
"mcp-server-env-rename": (c) =>
|
|
111
|
+
`MCP server ${q(c.subject)}${at(c.path)} maps environment variable ${q(c.detail)} to a differently named variable; Stigmer passes declared variables by name, so use the same name on both sides`,
|
|
112
|
+
"mcp-server-name-duplicate": (c) => `MCP server name ${q(c.subject)} appears more than once (again${at(c.path)})`,
|
|
113
|
+
"mcp-server-header-duplicate": (c) =>
|
|
114
|
+
`MCP server ${q(c.subject)}${at(c.path)} declares header ${q(c.detail)} more than once (header names are case-insensitive)`,
|
|
115
|
+
"mcp-server-header-invalid": (c) => `MCP server ${q(c.subject)}${at(c.path)} has an invalid header name ${q(c.detail)}`,
|
|
116
|
+
|
|
117
|
+
"sub-agent-frontmatter-unreadable": (c) => `${q(c.path)} frontmatter is not valid YAML: ${c.detail ?? "parse error"}`,
|
|
118
|
+
"sub-agent-instructions-short": (c) =>
|
|
119
|
+
`sub-agent ${q(c.subject)}${at(c.path)} has instructions under ${c.detail ?? ""} characters; the body of the file is the sub-agent's prompt`,
|
|
120
|
+
"sub-agent-name-duplicate": (c) => `sub-agent name ${q(c.subject)} appears more than once (again${at(c.path)})`,
|
|
121
|
+
|
|
122
|
+
"variable-name-invalid": (c) =>
|
|
123
|
+
`variable name ${q(c.subject)}${at(c.path)} is invalid: an environment variable name is letters, digits and underscores, not starting with a digit`,
|
|
124
|
+
|
|
125
|
+
"overlay-server-unknown": (c) =>
|
|
126
|
+
`${q(c.path)} overlays MCP server ${q(c.subject)}, which the plugin does not declare`,
|
|
127
|
+
"overlay-document-unknown": (c) =>
|
|
128
|
+
`${q(c.path)} is not a document Stigmer reads; the 'ai.stigmer/' folder holds 'agent.yaml', 'workflows/<name>.yaml' and 'mcp-servers/<server>.yaml'`,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const WARNING_MESSAGES: Record<PluginWarningKind, Sentence> = {
|
|
132
|
+
"manifest-field-unknown": (c) => `manifest ${q(c.path)} has an unknown field ${q(c.subject)}, ignored`,
|
|
133
|
+
"manifest-extensions-invalid": (c) => `manifest ${q(c.path)} has an 'extensions' field that is not an object, ignored`,
|
|
134
|
+
"path-missing": (c) => `path ${q(c.subject)} declared${at(c.path)} does not exist in the plugin`,
|
|
135
|
+
"skill-name-defaulted": (c) =>
|
|
136
|
+
`${q(c.path)} has no 'name' in its frontmatter; the skill is named after its directory, ${q(c.subject)}`,
|
|
137
|
+
"skill-name-differs-from-directory": (c) =>
|
|
138
|
+
`skill ${q(c.subject)}${at(c.path)} is named differently from its directory ${q(c.detail)}`,
|
|
139
|
+
"skill-description-missing": (c) => `skill ${q(c.subject)}${at(c.path)} has no 'description'`,
|
|
140
|
+
"mcp-config-field-ignored": (c) =>
|
|
141
|
+
`MCP configuration ${q(c.path)} has a top-level field ${q(c.subject)} Stigmer does not read, ignored`,
|
|
142
|
+
"mcp-server-sse-mapped": (c) =>
|
|
143
|
+
`MCP server ${q(c.subject)}${at(c.path)} declares the legacy 'sse' transport; Stigmer connects over Streamable HTTP and falls back to SSE only when the server rejects it`,
|
|
144
|
+
"mcp-server-field-ignored": (c) => `MCP server ${q(c.subject)}${at(c.path)} has a field ${q(c.detail)} Stigmer does not read, ignored`,
|
|
145
|
+
"mcp-server-auth-ignored": (c) =>
|
|
146
|
+
`MCP server ${q(c.subject)}${at(c.path)} has an 'auth' block Stigmer does not read; OAuth for a server is declared in 'ai.stigmer/mcp-servers/${c.subject ?? "<server>"}.yaml'`,
|
|
147
|
+
"variable-inferred": (c) =>
|
|
148
|
+
`variable ${q(c.subject)} is referenced by MCP server ${q(c.detail)} but not declared; it is declared as a required secret`,
|
|
149
|
+
"variable-unreferenced": (c) => `variable ${q(c.subject)}${at(c.path)} is declared but no MCP server references it`,
|
|
150
|
+
"variable-default-dropped": (c) =>
|
|
151
|
+
`variable ${q(c.subject)}${at(c.path)} has a default value, which Stigmer does not carry; the user supplies the value`,
|
|
152
|
+
"variable-type-narrowed": (c) =>
|
|
153
|
+
`variable ${q(c.subject)}${at(c.path)} is typed ${q(c.detail)}; Stigmer variables are strings`,
|
|
154
|
+
"variable-option-dropped": (c) =>
|
|
155
|
+
`variable ${q(c.subject)}${at(c.path)} has a ${q(c.detail)} constraint, which Stigmer does not carry`,
|
|
156
|
+
"sub-agent-name-defaulted": (c) =>
|
|
157
|
+
`${q(c.path)} has no 'name' in its frontmatter; the sub-agent is named after the file, ${q(c.subject)}`,
|
|
158
|
+
"sub-agent-skill-unknown": (c) =>
|
|
159
|
+
`sub-agent ${q(c.subject)}${at(c.path)} asks for skill ${q(c.detail)}, which the plugin does not ship`,
|
|
160
|
+
"sub-agent-model-unknown": (c) =>
|
|
161
|
+
`sub-agent ${q(c.subject)}${at(c.path)} names model ${q(c.detail)}, which Stigmer cannot map; the sub-agent runs on the session's model`,
|
|
162
|
+
"sub-agent-field-ignored": (c) => `sub-agent ${q(c.subject)}${at(c.path)} has a field ${q(c.detail)} Stigmer does not read, ignored`,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export function errorMessage(kind: PluginErrorKind, ctx: FindingContext): string {
|
|
166
|
+
return ERROR_MESSAGES[kind](ctx);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function warningMessage(kind: PluginWarningKind, ctx: FindingContext): string {
|
|
170
|
+
return WARNING_MESSAGES[kind](ctx);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** True for the kinds that refuse a package. */
|
|
174
|
+
export function isErrorKind(kind: PluginFindingKind): kind is PluginErrorKind {
|
|
175
|
+
return kind in ERROR_MESSAGES;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Collects findings while a read proceeds. The reader never throws on a
|
|
180
|
+
* plugin's content: every problem becomes a finding here, and the read
|
|
181
|
+
* continues so the outcome carries them all.
|
|
182
|
+
*/
|
|
183
|
+
export class Findings {
|
|
184
|
+
readonly errors: PluginFinding[] = [];
|
|
185
|
+
readonly warnings: PluginFinding[] = [];
|
|
186
|
+
|
|
187
|
+
error(kind: PluginErrorKind, ctx: FindingContext = {}): void {
|
|
188
|
+
this.errors.push(compose(kind, ctx, errorMessage(kind, ctx)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
warn(kind: PluginWarningKind, ctx: FindingContext = {}): void {
|
|
192
|
+
this.warnings.push(compose(kind, ctx, warningMessage(kind, ctx)));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Optional fields are omitted rather than set to `undefined` so a finding
|
|
197
|
+
// serialises to JSON without `null`s and compares structurally in tests.
|
|
198
|
+
function compose(kind: PluginFindingKind, ctx: FindingContext, message: string): PluginFinding {
|
|
199
|
+
return {
|
|
200
|
+
kind,
|
|
201
|
+
...(ctx.path !== undefined && { path: ctx.path }),
|
|
202
|
+
...(ctx.subject !== undefined && { subject: ctx.subject }),
|
|
203
|
+
...(ctx.detail !== undefined && { detail: ctx.detail }),
|
|
204
|
+
message,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The components Stigmer reads past, found on disk.
|
|
3
|
+
*
|
|
4
|
+
* Every dialect ships things that configure the IDE rather than describe
|
|
5
|
+
* knowledge or tools: rules, hooks, slash commands, canvases, output styles,
|
|
6
|
+
* themes, monitors, LSP servers, Codex apps, bundled binaries, a settings
|
|
7
|
+
* file, a logo. Stigmer installs none of them, and says so once per
|
|
8
|
+
* component (a directory or a file, never per file inside), so an author
|
|
9
|
+
* knows what an install leaves behind. Manifest fields that name the same
|
|
10
|
+
* components are recorded by the dialect readers; the two lists are merged
|
|
11
|
+
* and deduplicated by kind and path in `read-plugin-package.ts`.
|
|
12
|
+
*
|
|
13
|
+
* `agents/` joins the list only when no vendor manifest is present: the
|
|
14
|
+
* open format defines no sub-agent component, so a root-manifest-only
|
|
15
|
+
* plugin with an `agents/` folder is told the folder is not read rather
|
|
16
|
+
* than having semantics assigned to it. `README`, `CHANGELOG`, `LICENSE`
|
|
17
|
+
* and dotfiles are not components and pass silently.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { PluginFileIndex } from "../files.js";
|
|
21
|
+
import type { IgnoredComponent, IgnoredComponentKind } from "../types.js";
|
|
22
|
+
|
|
23
|
+
const IGNORED_DIRECTORIES: Readonly<Record<string, IgnoredComponentKind>> = {
|
|
24
|
+
rules: "rules",
|
|
25
|
+
hooks: "hooks",
|
|
26
|
+
commands: "commands",
|
|
27
|
+
canvases: "canvases",
|
|
28
|
+
workflows: "workflows",
|
|
29
|
+
"output-styles": "output-styles",
|
|
30
|
+
themes: "themes",
|
|
31
|
+
monitors: "monitors",
|
|
32
|
+
bin: "bin",
|
|
33
|
+
assets: "assets",
|
|
34
|
+
evals: "evals",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const IGNORED_FILES: Readonly<Record<string, IgnoredComponentKind>> = {
|
|
38
|
+
".lsp.json": "lsp-servers",
|
|
39
|
+
"settings.json": "settings",
|
|
40
|
+
".app.json": "apps",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function ignoredOnDisk(index: PluginFileIndex, readsAgents: boolean): readonly IgnoredComponent[] {
|
|
44
|
+
const ignored: IgnoredComponent[] = [];
|
|
45
|
+
for (const dir of index.childDirectories("")) {
|
|
46
|
+
const kind = IGNORED_DIRECTORIES[dir];
|
|
47
|
+
if (kind !== undefined) ignored.push({ kind, path: `${dir}/` });
|
|
48
|
+
if (dir === "agents" && !readsAgents) ignored.push({ kind: "agents", path: `${dir}/` });
|
|
49
|
+
}
|
|
50
|
+
for (const file of index.childFiles("")) {
|
|
51
|
+
const kind = IGNORED_FILES[file];
|
|
52
|
+
if (kind !== undefined) ignored.push({ kind, path: file });
|
|
53
|
+
}
|
|
54
|
+
return ignored;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One entry per (kind, path), in first-seen order. */
|
|
58
|
+
export function dedupeIgnored(lists: readonly (readonly IgnoredComponent[])[]): readonly IgnoredComponent[] {
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const out: IgnoredComponent[] = [];
|
|
61
|
+
for (const list of lists) {
|
|
62
|
+
for (const component of list) {
|
|
63
|
+
const key = `${component.kind}\u0000${component.path}`;
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
out.push(component);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|