zidane 6.4.4 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-BLO58K3F.js → acp-B-AOovHz.js} +2 -2
  3. package/dist/{acp-BLO58K3F.js.map → acp-B-AOovHz.js.map} +1 -1
  4. package/dist/acp-cli.js +3 -2
  5. package/dist/acp-cli.js.map +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-8sUadp5M.js → agent-SQ6rxfpT.js} +231 -106
  9. package/dist/agent-SQ6rxfpT.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -1
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat.d.ts +13 -44
  14. package/dist/chat.d.ts.map +1 -1
  15. package/dist/chat.js +5 -4
  16. package/dist/chat.js.map +1 -1
  17. package/dist/{context-transport-D29WvHcm.js → context-transport-CDBatvQc.js} +3 -3
  18. package/dist/{context-transport-D29WvHcm.js.map → context-transport-CDBatvQc.js.map} +1 -1
  19. package/dist/contexts/daytona.d.ts +1 -1
  20. package/dist/contexts/e2b.d.ts +1 -1
  21. package/dist/eval.d.ts +1 -1
  22. package/dist/extensions.d.ts +2 -2
  23. package/dist/extensions.js +2 -2
  24. package/dist/headless.d.ts +1 -1
  25. package/dist/headless.js +1 -1
  26. package/dist/{index-C-Nd2ZbV.d.ts → index-CWBSrqI6.d.ts} +676 -3
  27. package/dist/index-CWBSrqI6.d.ts.map +1 -0
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +7 -6
  30. package/dist/index.js.map +1 -1
  31. package/dist/{login-CMVVnAHS.js → login-XZDS4K2o.js} +3 -3
  32. package/dist/{login-CMVVnAHS.js.map → login-XZDS4K2o.js.map} +1 -1
  33. package/dist/{mcp-3GXUBwur.js → mcp-D27rP47I.js} +87 -6
  34. package/dist/mcp-D27rP47I.js.map +1 -0
  35. package/dist/mcp.d.ts +2 -2
  36. package/dist/mcp.js +3 -2
  37. package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
  38. package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
  39. package/dist/output/stream-json.d.ts +1 -1
  40. package/dist/output/terminal.d.ts +1 -1
  41. package/dist/plugins-2PMTVcp-.js +1174 -0
  42. package/dist/plugins-2PMTVcp-.js.map +1 -0
  43. package/dist/plugins.d.ts +2 -0
  44. package/dist/plugins.js +2 -0
  45. package/dist/presets.d.ts +1 -1
  46. package/dist/presets.js +1 -1
  47. package/dist/providers/anthropic.d.ts +1 -1
  48. package/dist/providers/arcee.d.ts +1 -1
  49. package/dist/providers/baseten.d.ts +1 -1
  50. package/dist/providers/cerebras.d.ts +1 -1
  51. package/dist/providers/local.d.ts +1 -1
  52. package/dist/providers/openai-compat.d.ts +1 -1
  53. package/dist/providers/openai.d.ts +1 -1
  54. package/dist/providers/openrouter.d.ts +1 -1
  55. package/dist/providers/xai.d.ts +1 -1
  56. package/dist/providers.d.ts +1 -1
  57. package/dist/restate.d.ts +1 -1
  58. package/dist/restate.js +5 -3
  59. package/dist/restate.js.map +1 -1
  60. package/dist/session/sqlite.d.ts +1 -1
  61. package/dist/session.d.ts +1 -1
  62. package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
  63. package/dist/skills-Cp9vnBwd.js.map +1 -0
  64. package/dist/skills.d.ts +1 -1
  65. package/dist/skills.js +1 -2
  66. package/dist/{stdio-BvL8HGYW.js → stdio-O-m9WVkE.js} +2 -2
  67. package/dist/{stdio-BvL8HGYW.js.map → stdio-O-m9WVkE.js.map} +1 -1
  68. package/dist/{stdio-loader-CjfoqBle.js → stdio-loader-BSGQmG4L.js} +2 -2
  69. package/dist/{stdio-loader-CjfoqBle.js.map → stdio-loader-BSGQmG4L.js.map} +1 -1
  70. package/dist/{tool-formatters-DPQjso_C.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
  71. package/dist/{tool-formatters-DPQjso_C.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
  72. package/dist/tools/fetch-url.d.ts +1 -1
  73. package/dist/tools/web-search.d.ts +1 -1
  74. package/dist/tools.d.ts +1 -1
  75. package/dist/tools.js +1 -1
  76. package/dist/{transcript-anchors-CgDzYin1.js → transcript-anchors-D4IxJifV.js} +42 -5
  77. package/dist/transcript-anchors-D4IxJifV.js.map +1 -0
  78. package/dist/{transcript-anchors-BCUpbNxc.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
  79. package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
  80. package/dist/tui.d.ts +38 -4
  81. package/dist/tui.d.ts.map +1 -1
  82. package/dist/tui.js +839 -105
  83. package/dist/tui.js.map +1 -1
  84. package/dist/{turn-operations-Bu2Hkinw.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
  85. package/dist/{turn-operations-Bu2Hkinw.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
  86. package/dist/types-BIarq1qE.js.map +1 -1
  87. package/dist/types.d.ts +1 -1
  88. package/docs/PLUGINS.md +340 -0
  89. package/docs/RESTATE.md +69 -3
  90. package/docs/SKILL.md +24 -8
  91. package/package.json +6 -1
  92. package/dist/agent-8sUadp5M.js.map +0 -1
  93. package/dist/index-C-Nd2ZbV.d.ts.map +0 -1
  94. package/dist/mcp-3GXUBwur.js.map +0 -1
  95. package/dist/resolve-CZJxds85.js.map +0 -1
  96. package/dist/skills-Dd6XCi4s.js +0 -420
  97. package/dist/skills-Dd6XCi4s.js.map +0 -1
  98. package/dist/transcript-anchors-BCUpbNxc.d.ts.map +0 -1
  99. package/dist/transcript-anchors-CgDzYin1.js.map +0 -1
@@ -0,0 +1,1174 @@
1
+ import { T as parseSkillFile } from "./skills-Cp9vnBwd.js";
2
+ import { t as isAllowedRemoteUrl } from "./origin-pinned-fetch-CqG8VZIq.js";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ //#region src/chat/xdg.ts
8
+ /**
9
+ * XDG Base Directory resolution for zidane.
10
+ *
11
+ * The user's storage spreads across four logical slots:
12
+ *
13
+ * - **config** — credentials, mcp-credentials, keybindings, config.json,
14
+ * mcps.json, skills (user-editable surface).
15
+ * - **data** — sessions.db (long-lived chat history).
16
+ * - **state** — state.json (per-machine UI bookkeeping: last provider,
17
+ * last model, last agent, settings).
18
+ * - **cache** — persisted tool results, background-task logs,
19
+ * auto-update registry cache (regenerable, safe to wipe).
20
+ *
21
+ * Historically all four collapsed to `~/.zidane/`. This module adds
22
+ * XDG-Base-Directory-aware defaults so a Linux user with no existing
23
+ * `~/.zidane/` lands in the conventional `~/.config/zidane/`,
24
+ * `~/.local/share/zidane/`, `~/.local/state/zidane/`, `~/.cache/zidane/`
25
+ * trio. Existing setups (anyone with a `~/.zidane/` directory or
26
+ * `ZIDANE_STORAGE_DIR` set) keep the single-dir layout unchanged.
27
+ *
28
+ * Resolution flow — applied in this order:
29
+ *
30
+ * 1. Caller passed an explicit `storageDir` (`ChatOptions.storageDir`)
31
+ * OR `ZIDANE_STORAGE_DIR` is set → **legacy-explicit** single-dir
32
+ * layout under `<storageDir>/<prefix>`. The four slots collapse to
33
+ * the same directory.
34
+ * 2. `~/<prefix>/` already exists on disk → **legacy-existing**
35
+ * single-dir layout under that directory. Upgrades never silently
36
+ * shift files around.
37
+ * 3. Any `XDG_*_HOME` env var is set OR `process.platform === 'linux'`
38
+ * → **XDG split**. Each slot lands in its own dir. Per-slot
39
+ * `ZIDANE_{CONFIG,DATA,CACHE,STATE}_DIR` overrides beat the XDG
40
+ * vars for surgical tweaks.
41
+ * 4. Otherwise (macOS/Windows, no XDG signal, no existing dir) →
42
+ * **default** single-dir layout at `~/<prefix>/`. Day-one behavior
43
+ * on these platforms is unchanged.
44
+ *
45
+ * The `userDir` legacy alias always equals `configDir` so existing code
46
+ * that still references `paths.userDir` keeps reading credentials, mcps,
47
+ * etc. from the right place.
48
+ */
49
+ /**
50
+ * Resolve the four storage slots according to the precedence above.
51
+ *
52
+ * Pure aside from one filesystem probe (`existsSync(home/<prefix>)`)
53
+ * — the same probe `resolveStoragePaths` would do anyway when
54
+ * computing `paths.userDir`. No directories are created here; the
55
+ * relevant write paths (state store, credentials writer, persist
56
+ * dir) handle their own lazy `mkdirSync` already.
57
+ */
58
+ function resolveStorageDirs(opts = {}) {
59
+ const env = opts.env ?? process.env;
60
+ const home = opts.home ?? homedir();
61
+ const platform = opts.platform ?? process.platform;
62
+ const rawPrefix = opts.prefix ?? ".zidane";
63
+ const prefixBare = rawPrefix.replace(/^\./, "");
64
+ const explicitStorageDir = opts.storageDir ?? env.ZIDANE_STORAGE_DIR;
65
+ if (explicitStorageDir) return single(resolve(explicitStorageDir, rawPrefix), "legacy-explicit");
66
+ const legacyHomeDir = resolve(home, rawPrefix);
67
+ if (existsSync(legacyHomeDir)) return single(legacyHomeDir, "legacy-existing");
68
+ if (platform === "linux" || !!env.XDG_CONFIG_HOME || !!env.XDG_DATA_HOME || !!env.XDG_CACHE_HOME || !!env.XDG_STATE_HOME || !!env.ZIDANE_CONFIG_DIR || !!env.ZIDANE_DATA_DIR || !!env.ZIDANE_STATE_DIR || !!env.ZIDANE_CACHE_DIR) return {
69
+ configDir: env.ZIDANE_CONFIG_DIR ?? resolve(env.XDG_CONFIG_HOME || resolve(home, ".config"), prefixBare),
70
+ dataDir: env.ZIDANE_DATA_DIR ?? resolve(env.XDG_DATA_HOME || resolve(home, ".local", "share"), prefixBare),
71
+ stateDir: env.ZIDANE_STATE_DIR ?? resolve(env.XDG_STATE_HOME || resolve(home, ".local", "state"), prefixBare),
72
+ cacheDir: env.ZIDANE_CACHE_DIR ?? resolve(env.XDG_CACHE_HOME || resolve(home, ".cache"), prefixBare),
73
+ mode: "xdg"
74
+ };
75
+ return single(legacyHomeDir, "default");
76
+ }
77
+ function single(base, mode) {
78
+ return {
79
+ configDir: base,
80
+ dataDir: base,
81
+ stateDir: base,
82
+ cacheDir: base,
83
+ mode
84
+ };
85
+ }
86
+ //#endregion
87
+ //#region src/plugins/boundary.ts
88
+ /**
89
+ * The Agent Plugins package boundary.
90
+ *
91
+ * The spec's central filesystem rule: "Every package file a client
92
+ * discovers, reads, or executes must remain inside the filesystem-resolved
93
+ * root after resolving symlinks, junctions, reparse points, and equivalent
94
+ * mechanisms."
95
+ *
96
+ * Lexical containment (`../skills/validate.ts`'s `validateResourcePath`) is
97
+ * NOT sufficient here — a `skills` symlink pointing at `~/.ssh` passes every
98
+ * lexical check. So each package path is resolved through `realpath` and
99
+ * re-tested against the realpath-resolved root, and the required filesystem
100
+ * kind is asserted with a symlink-FOLLOWING `stat` (the spec wants
101
+ * `SKILL.md` to be a regular file, not a fifo or a directory wearing the
102
+ * name).
103
+ *
104
+ * Containment is tested with `path.relative` rather than a string prefix so
105
+ * `/plugin-root` vs `/plugin-root-evil` can't be confused and Windows
106
+ * separators are handled by the platform.
107
+ */
108
+ /**
109
+ * Resolve the plugin root itself. Returns `null` when the root does not
110
+ * exist or is not a directory — the caller turns that into a plugin-level
111
+ * rejection, since there is nothing to load.
112
+ */
113
+ async function resolvePluginBoundary(dir) {
114
+ const absolute = resolve(dir);
115
+ try {
116
+ const root = await realpath(absolute);
117
+ if (!(await stat(root)).isDirectory()) return null;
118
+ return {
119
+ dir: absolute,
120
+ root
121
+ };
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * True when `real` (an already symlink-resolved path) lives at or under
128
+ * `root` (likewise resolved).
129
+ */
130
+ function isWithin(root, real) {
131
+ const rel = relative(root, real);
132
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
133
+ }
134
+ /**
135
+ * Symlink-resolve `target` and confirm it stays inside the boundary.
136
+ *
137
+ * Use this for paths whose filesystem kind doesn't matter or that may not
138
+ * exist yet (Phase 3's `cwd` handling); otherwise prefer
139
+ * {@link resolvePackagePath}, which also asserts the kind.
140
+ */
141
+ async function realPathWithin(boundary, target) {
142
+ let real;
143
+ try {
144
+ real = await realpath(target);
145
+ } catch {
146
+ return {
147
+ ok: false,
148
+ reason: "missing"
149
+ };
150
+ }
151
+ if (!isWithin(boundary.root, real)) return {
152
+ ok: false,
153
+ reason: "escaped"
154
+ };
155
+ return {
156
+ ok: true,
157
+ real
158
+ };
159
+ }
160
+ /**
161
+ * Resolve a package path and assert its filesystem kind.
162
+ *
163
+ * `stat` (not `lstat`) is deliberate: a symlink to a regular file INSIDE the
164
+ * root is legitimate packaging, and containment is already enforced against
165
+ * the resolved target. What we reject is the kind mismatch the spec calls
166
+ * out — `skills` as a regular file, `SKILL.md` as a directory or a fifo.
167
+ */
168
+ async function resolvePackagePath(boundary, target, kind) {
169
+ const within = await realPathWithin(boundary, target);
170
+ if (!within.ok) return within;
171
+ try {
172
+ const info = await stat(within.real);
173
+ if (!(kind === "directory" ? info.isDirectory() : info.isFile())) return {
174
+ ok: false,
175
+ reason: "wrong-kind"
176
+ };
177
+ } catch {
178
+ return {
179
+ ok: false,
180
+ reason: "missing"
181
+ };
182
+ }
183
+ return within;
184
+ }
185
+ //#endregion
186
+ //#region src/plugins/types.ts
187
+ /**
188
+ * Agent Plugins spec versions this loader implements, newest first.
189
+ *
190
+ * The spec requires clients to select validation rules from the manifest's
191
+ * `$schema` WITHOUT retrieving it, and to let plugins keep targeting older
192
+ * versions under a locally supported compatibility policy. Our policy: a
193
+ * version is supported iff it appears in this list. Adding `1.1.0` later
194
+ * means adding it here plus any rule deltas in `./manifest` — never
195
+ * reinterpreting an already-published identifier, which the spec forbids.
196
+ */
197
+ const SUPPORTED_AGENT_PLUGINS_VERSIONS = ["1.0.0"];
198
+ /**
199
+ * Canonical schema identifier for a given document kind + version. Built
200
+ * rather than hardcoded so the two call sites (manifest, mcp.json) can't
201
+ * drift, and so the version cross-check the spec requires between
202
+ * `plugin.json` and `mcp.json` is a plain string comparison.
203
+ */
204
+ function agentPluginsSchemaId(kind, version) {
205
+ return `https://agent-plugins.org/schemas/${version}/${kind}.schema.json`;
206
+ }
207
+ /**
208
+ * Canonical schema identifier grammar. Anchored and exact — a `$schema`
209
+ * that merely looks similar (http, a trailing slash, a different host) is
210
+ * not the canonical identifier and must not select the contract.
211
+ */
212
+ const SCHEMA_ID_RE = /^https:\/\/agent-plugins\.org\/schemas\/([^/]+)\/(plugin|mcp)\.schema\.json$/;
213
+ /**
214
+ * Split a canonical `$schema` identifier into its document kind + version.
215
+ * Returns `null` for anything that isn't a canonical identifier; the caller
216
+ * decides whether that's fatal (manifest) or component-scoped (mcp.json).
217
+ *
218
+ * Purely syntactic: it never fetches. The spec forbids retrieving a schema
219
+ * during load, so version support is a lookup against
220
+ * {@link SUPPORTED_AGENT_PLUGINS_VERSIONS}, not a download.
221
+ */
222
+ function parseSchemaId(value) {
223
+ const match = SCHEMA_ID_RE.exec(value);
224
+ if (!match) return null;
225
+ return {
226
+ kind: match[2],
227
+ version: match[1]
228
+ };
229
+ }
230
+ /** True when this loader implements the given Agent Plugins version. */
231
+ function isSupportedAgentPluginsVersion(version) {
232
+ return SUPPORTED_AGENT_PLUGINS_VERSIONS.includes(version);
233
+ }
234
+ /**
235
+ * Type label for a rejected JSON value, for diagnostic messages.
236
+ *
237
+ * `typeof` alone is useless in a validator that has to tell an author what
238
+ * they got wrong: `null` and `[]` both report `'object'`, and those are two of
239
+ * the most common mistakes in a hand-written manifest. Shared by the manifest
240
+ * and `mcp.json` validators, which produce the same class of message.
241
+ *
242
+ * Internal — not re-exported from the module barrel.
243
+ */
244
+ function describeJsonType(value) {
245
+ if (value === null) return "null";
246
+ if (Array.isArray(value)) return "array";
247
+ return typeof value;
248
+ }
249
+ //#endregion
250
+ //#region src/plugins/manifest.ts
251
+ /**
252
+ * Plugin name grammar, lifted from the canonical schema: 1-64 chars,
253
+ * lowercase alphanumerics plus `-` and `.`, alphanumeric at both ends, and
254
+ * no `--` or `..` anywhere (the negative lookahead).
255
+ *
256
+ * Length is checked separately so an over-long name gets a message about
257
+ * length instead of a generic "doesn't match the grammar".
258
+ */
259
+ const PLUGIN_NAME_RE = /^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
260
+ const NAME_MAX = 64;
261
+ /** Top-level fields the 1.0.0 manifest permits. Anything else is unknown. */
262
+ const KNOWN_FIELDS = /* @__PURE__ */ new Set([
263
+ "$schema",
264
+ "name",
265
+ "version",
266
+ "description",
267
+ "author",
268
+ "homepage",
269
+ "repository",
270
+ "license",
271
+ "keywords",
272
+ "extensions"
273
+ ]);
274
+ /** Optional top-level fields whose value must be a string when present. */
275
+ const STRING_FIELDS = [
276
+ "version",
277
+ "description",
278
+ "homepage",
279
+ "repository",
280
+ "license"
281
+ ];
282
+ /** Keys the closed `author` object permits. */
283
+ const AUTHOR_FIELDS = /* @__PURE__ */ new Set([
284
+ "name",
285
+ "email",
286
+ "url"
287
+ ]);
288
+ /**
289
+ * Parse manifest text, then validate it.
290
+ *
291
+ * A JSON syntax error is fatal — unlike `mcps.json` (where
292
+ * `parseJsonTolerant` repairs copy-paste whitespace artifacts), a plugin
293
+ * manifest is a package file produced by a build or an author's editor, not
294
+ * pasted into a settings box. Repairing it would mean guessing at the
295
+ * contents of a third-party package.
296
+ */
297
+ function parsePluginManifest(text) {
298
+ let raw;
299
+ try {
300
+ raw = JSON.parse(text);
301
+ } catch (err) {
302
+ return {
303
+ ok: false,
304
+ diagnostics: [{
305
+ severity: "error",
306
+ code: "manifest-invalid-json",
307
+ message: `plugin.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
308
+ }]
309
+ };
310
+ }
311
+ return validatePluginManifest(raw);
312
+ }
313
+ /**
314
+ * Validate an already-parsed manifest value against the closed 1.0.0 schema.
315
+ *
316
+ * Fatal violations short-circuit: `$schema` and `name` are checked first
317
+ * because without them there is no contract to validate against and no
318
+ * identity to report problems under.
319
+ */
320
+ function validatePluginManifest(raw) {
321
+ const diagnostics = [];
322
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
323
+ ok: false,
324
+ diagnostics: [{
325
+ severity: "error",
326
+ code: "manifest-not-object",
327
+ message: `plugin.json must be a JSON object (got ${Array.isArray(raw) ? "array" : typeof raw}).`
328
+ }]
329
+ };
330
+ const obj = raw;
331
+ const schema = obj.$schema;
332
+ if (typeof schema !== "string" || schema.length === 0) return fatal(diagnostics, {
333
+ code: "manifest-schema-missing",
334
+ message: `plugin.json is missing the required "$schema" field. Expected "${agentPluginsSchemaId("plugin", SUPPORTED_AGENT_PLUGINS_VERSIONS[0])}".`,
335
+ field: "$schema"
336
+ });
337
+ const parsed = parseSchemaId(schema);
338
+ if (!parsed || parsed.kind !== "plugin") return fatal(diagnostics, {
339
+ code: "manifest-schema-unsupported",
340
+ message: `plugin.json "$schema" is not a canonical Agent Plugins manifest schema identifier: "${schema}".`,
341
+ field: "$schema"
342
+ });
343
+ if (!isSupportedAgentPluginsVersion(parsed.version)) return fatal(diagnostics, {
344
+ code: "manifest-schema-unsupported",
345
+ message: `plugin.json targets Agent Plugins ${parsed.version}, which this client does not support (supported: ${SUPPORTED_AGENT_PLUGINS_VERSIONS.join(", ")}).`,
346
+ field: "$schema"
347
+ });
348
+ const name = obj.name;
349
+ if (typeof name !== "string") return fatal(diagnostics, {
350
+ code: "manifest-name-missing",
351
+ message: `plugin.json is missing the required "name" field${name === void 0 ? "" : ` (got ${typeof name})`}.`,
352
+ field: "name"
353
+ });
354
+ if (name.length === 0 || name.length > NAME_MAX) return fatal(diagnostics, {
355
+ code: "manifest-name-invalid",
356
+ message: `Plugin name must be 1-${NAME_MAX} characters (got ${name.length}).`,
357
+ field: "name"
358
+ });
359
+ if (!PLUGIN_NAME_RE.test(name)) return fatal(diagnostics, {
360
+ code: "manifest-name-invalid",
361
+ message: `Plugin name "${name}" is invalid — use lowercase letters, digits, hyphens and periods, starting and ending alphanumeric, with no "--" or "..".`,
362
+ field: "name"
363
+ });
364
+ const manifest = {
365
+ specVersion: parsed.version,
366
+ name
367
+ };
368
+ for (const field of STRING_FIELDS) {
369
+ const value = obj[field];
370
+ if (value === void 0) continue;
371
+ if (typeof value !== "string") return fatal(diagnostics, {
372
+ code: "manifest-field-wrong-type",
373
+ message: `plugin.json "${field}" must be a string (got ${describeJsonType(value)}).`,
374
+ field
375
+ });
376
+ manifest[field] = value;
377
+ }
378
+ if (obj.keywords !== void 0) {
379
+ if (!Array.isArray(obj.keywords)) return fatal(diagnostics, {
380
+ code: "manifest-field-wrong-type",
381
+ message: `plugin.json "keywords" must be an array of strings (got ${describeJsonType(obj.keywords)}).`,
382
+ field: "keywords"
383
+ });
384
+ const bad = obj.keywords.findIndex((entry) => typeof entry !== "string");
385
+ if (bad !== -1) return fatal(diagnostics, {
386
+ code: "manifest-field-wrong-type",
387
+ message: `plugin.json "keywords[${bad}]" must be a string (got ${describeJsonType(obj.keywords[bad])}).`,
388
+ field: "keywords"
389
+ });
390
+ manifest.keywords = [...obj.keywords];
391
+ }
392
+ if (obj.author !== void 0) {
393
+ const author = obj.author;
394
+ if (typeof author !== "object" || author === null || Array.isArray(author)) return fatal(diagnostics, {
395
+ code: "manifest-field-wrong-type",
396
+ message: `plugin.json "author" must be an object (got ${describeJsonType(author)}).`,
397
+ field: "author"
398
+ });
399
+ const out = {};
400
+ for (const [key, value] of Object.entries(author)) {
401
+ if (!AUTHOR_FIELDS.has(key)) return fatal(diagnostics, {
402
+ code: "manifest-author-unknown-field",
403
+ message: `plugin.json "author.${key}" is not a permitted field (allowed: ${[...AUTHOR_FIELDS].join(", ")}).`,
404
+ field: "author"
405
+ });
406
+ if (typeof value !== "string") return fatal(diagnostics, {
407
+ code: "manifest-field-wrong-type",
408
+ message: `plugin.json "author.${key}" must be a string (got ${describeJsonType(value)}).`,
409
+ field: "author"
410
+ });
411
+ out[key] = value;
412
+ }
413
+ manifest.author = out;
414
+ }
415
+ if (obj.extensions !== void 0) {
416
+ const ext = obj.extensions;
417
+ if (typeof ext !== "object" || ext === null || Array.isArray(ext)) diagnostics.push({
418
+ severity: "warning",
419
+ code: "manifest-extensions-not-object",
420
+ message: `plugin.json "extensions" must be an object (got ${describeJsonType(ext)}) — ignoring it and continuing to load components.`,
421
+ field: "extensions"
422
+ });
423
+ else manifest.extensions = { ...ext };
424
+ }
425
+ for (const key of Object.keys(obj)) {
426
+ if (KNOWN_FIELDS.has(key)) continue;
427
+ diagnostics.push({
428
+ severity: "warning",
429
+ code: "manifest-unknown-field",
430
+ message: `plugin.json has unknown top-level field "${key}" — ignoring it. Client-specific data belongs under "extensions".`,
431
+ field: key
432
+ });
433
+ }
434
+ return {
435
+ ok: true,
436
+ manifest,
437
+ diagnostics
438
+ };
439
+ }
440
+ function fatal(diagnostics, detail) {
441
+ diagnostics.push({
442
+ severity: "error",
443
+ ...detail
444
+ });
445
+ return {
446
+ ok: false,
447
+ diagnostics
448
+ };
449
+ }
450
+ //#endregion
451
+ //#region src/plugins/mcp.ts
452
+ /** Fixed location for the MCP component type. */
453
+ const PLUGIN_MCP_FILE = "mcp.json";
454
+ /** The only placeholders the spec defines. */
455
+ const PLUGIN_ROOT_VAR = "PLUGIN_ROOT";
456
+ const PLUGIN_DATA_VAR = "PLUGIN_DATA";
457
+ /**
458
+ * Placeholder grammar — deliberately narrower than
459
+ * `expandEnvPlaceholders`'s: exactly the two reserved names, no
460
+ * `:-default` form, no `${env:…}` form. Anything else stays literal.
461
+ */
462
+ const PLUGIN_VAR_RE = /\$\{(PLUGIN_ROOT|PLUGIN_DATA)\}/g;
463
+ /** Closed top-level key set for the document. */
464
+ const DOCUMENT_FIELDS = /* @__PURE__ */ new Set(["$schema", "mcpServers"]);
465
+ /**
466
+ * Closed key set per declared transport.
467
+ *
468
+ * A `Map` rather than an object literal: the keys are attacker-controlled
469
+ * (`type` comes straight out of a third-party `mcp.json`), and `'constructor'
470
+ * in objectLiteral` is `true`, which would sail past the membership check and
471
+ * then blow up on `Object.prototype.constructor.has(...)`.
472
+ */
473
+ const ENTRY_FIELDS = /* @__PURE__ */ new Map([
474
+ ["stdio", /* @__PURE__ */ new Set([
475
+ "type",
476
+ "command",
477
+ "args",
478
+ "env",
479
+ "cwd"
480
+ ])],
481
+ ["streamable-http", /* @__PURE__ */ new Set([
482
+ "type",
483
+ "url",
484
+ "headers"
485
+ ])],
486
+ ["sse", /* @__PURE__ */ new Set([
487
+ "type",
488
+ "url",
489
+ "headers"
490
+ ])]
491
+ ]);
492
+ /**
493
+ * Server-name grammar.
494
+ *
495
+ * `connectMcpServers` namespaces tools as `mcp_<server>_<tool>`, and providers
496
+ * reject a tool name outside the letters / digits / underscore / hyphen shape
497
+ * — rejecting the whole REQUEST, not just the offending tool. A user-authored
498
+ * `mcps.json` has the same sharp edge, but there the user owns the file; a
499
+ * plugin is untrusted input, so an unusable name is rejected per-entry instead
500
+ * of poisoning every run.
501
+ */
502
+ const SERVER_NAME_RE = /^[a-z0-9][\w-]{0,63}$/i;
503
+ /**
504
+ * Keys that hit a prototype setter instead of creating an own property.
505
+ * Same guard the extension registration paths apply to untrusted keys — see
506
+ * `isSafeRecordKey` in `../extensions/register.ts`. Assigning `__proto__` on
507
+ * an object literal silently DISCARDS the value, so without this an env var
508
+ * would just never reach the child process with nothing explaining why.
509
+ */
510
+ const DANGEROUS_RECORD_KEYS = /* @__PURE__ */ new Set([
511
+ "__proto__",
512
+ "prototype",
513
+ "constructor"
514
+ ]);
515
+ /**
516
+ * Load and validate the plugin's `mcp.json`.
517
+ *
518
+ * Status semantics mirror `loadPluginSkills`: a missing file is `absent`
519
+ * (valid), a wrong-kind / escaping / malformed document is `invalid` (this
520
+ * component only), and anything else is `loaded` — even when individual
521
+ * entries were rejected, because per-entry isolation is the point.
522
+ */
523
+ async function loadPluginMcp(opts) {
524
+ const { boundary, pluginName, dataDir, specVersion, namePrefix } = opts;
525
+ const diagnostics = [];
526
+ const path = resolve(boundary.root, PLUGIN_MCP_FILE);
527
+ const located = await realPathWithin(boundary, path);
528
+ if (!located.ok) {
529
+ if (located.reason === "missing") return {
530
+ status: "absent",
531
+ servers: [],
532
+ diagnostics
533
+ };
534
+ diagnostics.push({
535
+ severity: "error",
536
+ code: "mcp-file-escapes-root",
537
+ message: `"${PLUGIN_MCP_FILE}" resolves outside the plugin root — the MCP component is disabled for this plugin.`,
538
+ path
539
+ });
540
+ return {
541
+ status: "invalid",
542
+ servers: [],
543
+ diagnostics
544
+ };
545
+ }
546
+ let raw;
547
+ try {
548
+ raw = JSON.parse(await readFile(located.real, "utf8"));
549
+ } catch (err) {
550
+ diagnostics.push({
551
+ severity: "error",
552
+ code: "mcp-invalid-json",
553
+ message: `"${PLUGIN_MCP_FILE}" could not be read as JSON: ${err instanceof Error ? err.message : String(err)}`,
554
+ path: located.real
555
+ });
556
+ return {
557
+ status: "invalid",
558
+ servers: [],
559
+ diagnostics
560
+ };
561
+ }
562
+ const document = validateDocument(raw, specVersion);
563
+ if (!document.ok) {
564
+ diagnostics.push({
565
+ severity: "error",
566
+ ...document.diagnostic,
567
+ path: located.real
568
+ });
569
+ return {
570
+ status: "invalid",
571
+ servers: [],
572
+ diagnostics
573
+ };
574
+ }
575
+ const servers = [];
576
+ for (const [name, entry] of Object.entries(document.servers)) {
577
+ const built = await buildServer({
578
+ name,
579
+ entry,
580
+ boundary,
581
+ dataDir,
582
+ pluginName,
583
+ ...namePrefix !== void 0 ? { namePrefix } : {}
584
+ });
585
+ if (!built.ok) {
586
+ diagnostics.push({
587
+ severity: "error",
588
+ ...built.diagnostic,
589
+ path: located.real
590
+ });
591
+ continue;
592
+ }
593
+ servers.push(built.config);
594
+ }
595
+ return {
596
+ status: "loaded",
597
+ servers,
598
+ diagnostics
599
+ };
600
+ }
601
+ function validateDocument(raw, specVersion) {
602
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
603
+ ok: false,
604
+ diagnostic: {
605
+ code: "mcp-not-object",
606
+ message: `"${PLUGIN_MCP_FILE}" must be a JSON object (got ${describeJsonType(raw)}).`
607
+ }
608
+ };
609
+ const obj = raw;
610
+ const schema = obj.$schema;
611
+ if (typeof schema !== "string") return {
612
+ ok: false,
613
+ diagnostic: {
614
+ code: "mcp-schema-missing",
615
+ message: `"${PLUGIN_MCP_FILE}" is missing the required "$schema" field. Expected "${agentPluginsSchemaId("mcp", specVersion)}".`,
616
+ field: "$schema"
617
+ }
618
+ };
619
+ const parsed = parseSchemaId(schema);
620
+ if (!parsed || parsed.kind !== "mcp") return {
621
+ ok: false,
622
+ diagnostic: {
623
+ code: "mcp-schema-unsupported",
624
+ message: `"${PLUGIN_MCP_FILE}" "$schema" is not a canonical Agent Plugins MCP schema identifier: "${schema}".`,
625
+ field: "$schema"
626
+ }
627
+ };
628
+ if (parsed.version !== specVersion) return {
629
+ ok: false,
630
+ diagnostic: {
631
+ code: "mcp-schema-version-mismatch",
632
+ message: `"${PLUGIN_MCP_FILE}" targets Agent Plugins ${parsed.version} but plugin.json targets ${specVersion} — the versions must match.`,
633
+ field: "$schema"
634
+ }
635
+ };
636
+ for (const key of Object.keys(obj)) if (!DOCUMENT_FIELDS.has(key)) return {
637
+ ok: false,
638
+ diagnostic: {
639
+ code: "mcp-unknown-field",
640
+ message: `"${PLUGIN_MCP_FILE}" has unknown top-level field "${key}".`,
641
+ field: key
642
+ }
643
+ };
644
+ const servers = obj.mcpServers;
645
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return {
646
+ ok: false,
647
+ diagnostic: {
648
+ code: "mcp-servers-wrong-type",
649
+ message: `"${PLUGIN_MCP_FILE}" "mcpServers" must be an object (got ${describeJsonType(servers)}).`,
650
+ field: "mcpServers"
651
+ }
652
+ };
653
+ return {
654
+ ok: true,
655
+ servers
656
+ };
657
+ }
658
+ async function buildServer(opts) {
659
+ const { name, entry, boundary, dataDir, namePrefix } = opts;
660
+ const reject = (code, message) => ({
661
+ ok: false,
662
+ diagnostic: {
663
+ code,
664
+ message: `Skipped MCP server "${name}" — ${message}`
665
+ }
666
+ });
667
+ const serverName = namePrefix ? `${namePrefix}-${name}` : name;
668
+ if (!SERVER_NAME_RE.test(serverName)) return reject("mcp-entry-name-invalid", namePrefix ? `its namespaced name "${serverName}" is not usable as a tool prefix — the "${namePrefix}" namePrefix must be letters, digits, underscores or hyphens, and the two together at most 64 characters.` : "its name must be 1-64 characters of letters, digits, underscores or hyphens, starting alphanumeric.");
669
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return reject("mcp-entry-not-object", `its value must be an object (got ${describeJsonType(entry)}).`);
670
+ const obj = entry;
671
+ const type = typeof obj.type === "string" ? obj.type : void 0;
672
+ const allowed = type === void 0 ? void 0 : ENTRY_FIELDS.get(type);
673
+ if (type === void 0 || !allowed) return reject("mcp-entry-transport-invalid", `"type" must be one of stdio, streamable-http, sse (got ${type === void 0 ? describeJsonType(obj.type) : `"${type}"`}).`);
674
+ for (const key of Object.keys(obj)) if (!allowed.has(key)) return reject("mcp-entry-unknown-field", `"${key}" is not a permitted field for a ${type} server (allowed: ${[...allowed].join(", ")}).`);
675
+ const vars = {
676
+ [PLUGIN_ROOT_VAR]: boundary.root,
677
+ [PLUGIN_DATA_VAR]: dataDir
678
+ };
679
+ if (type === "stdio") {
680
+ const command = obj.command;
681
+ if (typeof command !== "string" || command.length === 0) return reject("mcp-entry-command-invalid", "a stdio server needs a non-empty \"command\".");
682
+ if (PLUGIN_VAR_RE.test(command)) {
683
+ PLUGIN_VAR_RE.lastIndex = 0;
684
+ return reject("mcp-entry-command-invalid", "placeholders are not expanded in \"command\" — use a \"./\"-relative path or a bare executable name.");
685
+ }
686
+ if (/\s/.test(command)) return reject("mcp-entry-command-invalid", `"command" must be a single executable token, not a command line ("${command}").`);
687
+ const resolvedCommand = await resolveCommand(command, boundary);
688
+ if (!resolvedCommand.ok) return reject("mcp-entry-command-invalid", resolvedCommand.error);
689
+ let args;
690
+ if (obj.args !== void 0) {
691
+ if (!Array.isArray(obj.args) || obj.args.some((a) => typeof a !== "string")) return reject("mcp-entry-args-invalid", "\"args\" must be an array of strings.");
692
+ args = obj.args.map((a) => expandPluginVars(a, vars));
693
+ }
694
+ let env;
695
+ if (obj.env !== void 0) {
696
+ if (typeof obj.env !== "object" || obj.env === null || Array.isArray(obj.env)) return reject("mcp-entry-env-invalid", "\"env\" must be an object of strings.");
697
+ env = {};
698
+ for (const [key, value] of Object.entries(obj.env)) {
699
+ if (typeof value !== "string") return reject("mcp-entry-env-invalid", `"env.${key}" must be a string (got ${describeJsonType(value)}).`);
700
+ if (key === PLUGIN_ROOT_VAR || key === PLUGIN_DATA_VAR) return reject("mcp-entry-env-invalid", `"env.${key}" is reserved and cannot be set by a plugin.`);
701
+ if (!isSafeRecordKey(key)) return reject("mcp-entry-env-invalid", `"env.${key}" is not a usable variable name.`);
702
+ env[key] = expandPluginVars(value, vars);
703
+ }
704
+ }
705
+ const cwdResult = await resolveCwd(obj.cwd, boundary, dataDir, vars);
706
+ if (!cwdResult.ok) return reject("mcp-entry-cwd-invalid", cwdResult.error);
707
+ const config = {
708
+ name: serverName,
709
+ transport: "stdio",
710
+ command: resolvedCommand.command,
711
+ env: {
712
+ ...env,
713
+ [PLUGIN_ROOT_VAR]: boundary.root,
714
+ [PLUGIN_DATA_VAR]: dataDir
715
+ },
716
+ cwd: cwdResult.cwd,
717
+ preExpanded: true
718
+ };
719
+ if (args) config.args = args;
720
+ return {
721
+ ok: true,
722
+ config
723
+ };
724
+ }
725
+ const url = obj.url;
726
+ if (typeof url !== "string" || url.length === 0) return reject("mcp-entry-url-invalid", `a ${type} server needs a non-empty "url".`);
727
+ const checked = validateRemoteUrl(url);
728
+ if (!checked.ok) return reject("mcp-entry-url-invalid", checked.error);
729
+ let headers;
730
+ if (obj.headers !== void 0) {
731
+ if (typeof obj.headers !== "object" || obj.headers === null || Array.isArray(obj.headers)) return reject("mcp-entry-headers-invalid", "\"headers\" must be an object of strings.");
732
+ headers = {};
733
+ for (const [key, value] of Object.entries(obj.headers)) {
734
+ if (!isSafeRecordKey(key)) return reject("mcp-entry-headers-invalid", `"headers.${key}" is not a usable header name.`);
735
+ if (typeof value !== "string") return reject("mcp-entry-headers-invalid", `"headers.${key}" must be a string (got ${describeJsonType(value)}).`);
736
+ headers[key] = value;
737
+ }
738
+ }
739
+ const config = {
740
+ name: serverName,
741
+ transport: type === "sse" ? "sse" : "streamable-http",
742
+ url,
743
+ preExpanded: true,
744
+ pinHeaderOrigin: true
745
+ };
746
+ if (headers) config.headers = headers;
747
+ return {
748
+ ok: true,
749
+ config
750
+ };
751
+ }
752
+ /**
753
+ * Substitute the two reserved placeholders in one string.
754
+ *
755
+ * Single-pass by construction (`String.replace` never re-scans its own
756
+ * output), so a `${PLUGIN_ROOT}` whose value somehow contained
757
+ * `${PLUGIN_DATA}` cannot expand twice. Unknown `${...}` text is left
758
+ * verbatim: the spec defines exactly two placeholders, and silently
759
+ * blanking anything else would corrupt legitimate literals.
760
+ */
761
+ function expandPluginVars(value, vars) {
762
+ return value.replace(PLUGIN_VAR_RE, (match, name) => vars[name] ?? match);
763
+ }
764
+ /**
765
+ * Resolve the single `command` token.
766
+ *
767
+ * A bare name (`uvx`, `node`) is handed to the OS for PATH lookup — the
768
+ * plugin declares a dependency on the host's toolchain, which is normal. A
769
+ * `./`-relative token is a file the PACKAGE ships, so it resolves against
770
+ * the plugin root and must survive the containment check. An absolute path
771
+ * is refused: it is neither a PATH lookup nor a package file, and it is the
772
+ * shape an escape attempt takes.
773
+ */
774
+ async function resolveCommand(command, boundary) {
775
+ if (isAbsolute(command)) return {
776
+ ok: false,
777
+ error: `"command" must be a bare executable name or a "./"-relative path inside the plugin, not an absolute path ("${command}").`
778
+ };
779
+ if (!command.includes("/") && !command.includes("\\")) return {
780
+ ok: true,
781
+ command
782
+ };
783
+ const within = await realPathWithin(boundary, resolve(boundary.root, command));
784
+ if (!within.ok) return {
785
+ ok: false,
786
+ error: within.reason === "escaped" ? `"command" resolves outside the plugin root ("${command}").` : `"command" does not exist inside the plugin ("${command}").`
787
+ };
788
+ return {
789
+ ok: true,
790
+ command: within.real
791
+ };
792
+ }
793
+ /**
794
+ * Resolve `cwd`, defaulting to the plugin root.
795
+ *
796
+ * The authored form is constrained to `./…`, `${PLUGIN_ROOT}…` or
797
+ * `${PLUGIN_DATA}…`, and containment is then verified against BOTH permitted
798
+ * roots — the data directory legitimately sits outside the package.
799
+ *
800
+ * Both a lexical and a post-symlink check run, because neither alone is
801
+ * sufficient: `cwd` may not exist yet (a server that creates its own working
802
+ * directory on first run), so realpath cannot be the only gate; but the data
803
+ * directory is writable BY the MCP server, so a server can plant a
804
+ * `${PLUGIN_DATA}/work → /` symlink on one run and get `cwd=/` on the next
805
+ * unless the resolved target is re-checked.
806
+ */
807
+ async function resolveCwd(raw, boundary, dataDir, vars) {
808
+ if (raw === void 0) return {
809
+ ok: true,
810
+ cwd: boundary.root
811
+ };
812
+ if (typeof raw !== "string" || raw.length === 0) return {
813
+ ok: false,
814
+ error: "\"cwd\" must be a non-empty string."
815
+ };
816
+ const startsWithVar = raw.startsWith(`\${${PLUGIN_ROOT_VAR}}`) || raw.startsWith(`\${${PLUGIN_DATA_VAR}}`);
817
+ if (!raw.startsWith("./") && !startsWithVar) return {
818
+ ok: false,
819
+ error: `"cwd" must start with "./", "\${${PLUGIN_ROOT_VAR}}" or "\${${PLUGIN_DATA_VAR}}" (got "${raw}").`
820
+ };
821
+ const expanded = expandPluginVars(raw, vars);
822
+ const absolute = isAbsolute(expanded) ? expanded : resolve(boundary.root, expanded);
823
+ if (!isWithin(boundary.root, absolute) && !isWithin(dataDir, absolute)) return {
824
+ ok: false,
825
+ error: `"cwd" escapes both the plugin root and the plugin data directory ("${raw}").`
826
+ };
827
+ const real = await realpathOrNull(absolute);
828
+ if (real !== null) {
829
+ const realDataDir = await realpathOrNull(dataDir) ?? dataDir;
830
+ if (!isWithin(boundary.root, real) && !isWithin(realDataDir, real)) return {
831
+ ok: false,
832
+ error: `"cwd" resolves outside the plugin root and the plugin data directory via a symlink ("${raw}").`
833
+ };
834
+ }
835
+ return {
836
+ ok: true,
837
+ cwd: absolute
838
+ };
839
+ }
840
+ /** `realpath`, or `null` when the path doesn't exist yet. */
841
+ async function realpathOrNull(target) {
842
+ try {
843
+ return await realpath(target);
844
+ } catch {
845
+ return null;
846
+ }
847
+ }
848
+ /**
849
+ * Enforce the spec's remote-endpoint requirements: parseable, HTTPS unless
850
+ * loopback, no embedded credentials, no fragment.
851
+ *
852
+ * Reachability (scheme + host) is decided by {@link isAllowedRemoteUrl}, the
853
+ * single encoding of that policy — `originPinnedFetch` applies the same
854
+ * predicate per redirect hop, so a URL refused here can't be reached by
855
+ * redirect either. Only the message selection lives here; the userinfo and
856
+ * fragment rules are authoring-only and have no redirect equivalent.
857
+ */
858
+ function validateRemoteUrl(raw) {
859
+ let url;
860
+ try {
861
+ url = new URL(raw);
862
+ } catch {
863
+ return {
864
+ ok: false,
865
+ error: `"url" is not a valid absolute URL ("${raw}").`
866
+ };
867
+ }
868
+ if (!isAllowedRemoteUrl(url)) return {
869
+ ok: false,
870
+ error: url.protocol === "http:" ? `"url" must use https for non-loopback hosts ("${raw}").` : `"url" must use http or https (got "${url.protocol}").`
871
+ };
872
+ if (url.username !== "" || url.password !== "") return {
873
+ ok: false,
874
+ error: "\"url\" must not embed userinfo credentials."
875
+ };
876
+ if (url.hash !== "") return {
877
+ ok: false,
878
+ error: "\"url\" must not contain a fragment."
879
+ };
880
+ return { ok: true };
881
+ }
882
+ /** Reject keys that would hit a prototype setter instead of the record. */
883
+ function isSafeRecordKey(key) {
884
+ return key.length > 0 && !DANGEROUS_RECORD_KEYS.has(key);
885
+ }
886
+ //#endregion
887
+ //#region src/plugins/skills.ts
888
+ /** Fixed location for the skills component type. */
889
+ const PLUGIN_SKILLS_DIR = "skills";
890
+ /** Spec cap on a skill name, re-checked after a `namePrefix` is applied. */
891
+ const SKILL_NAME_MAX = 64;
892
+ /**
893
+ * Discover every skill in the plugin's `skills/` directory.
894
+ *
895
+ * Status semantics follow the spec's failure ladder:
896
+ * - `skills/` missing → `absent` (valid; not an error)
897
+ * - `skills/` not a directory → `invalid` (this component type only)
898
+ * - `skills/` escapes the root → `invalid` (this component type only)
899
+ * - otherwise → `loaded`, possibly with zero skills
900
+ */
901
+ async function loadPluginSkills(opts) {
902
+ const { boundary, pluginName, source = "project", namePrefix } = opts;
903
+ const diagnostics = [];
904
+ const dir = join(boundary.root, PLUGIN_SKILLS_DIR);
905
+ const located = await resolvePackagePath(boundary, dir, "directory");
906
+ if (!located.ok) {
907
+ if (located.reason === "missing") return {
908
+ status: "absent",
909
+ skills: [],
910
+ diagnostics
911
+ };
912
+ diagnostics.push({
913
+ severity: "error",
914
+ code: located.reason === "escaped" ? "skills-dir-escapes-root" : "skills-dir-wrong-kind",
915
+ message: located.reason === "escaped" ? `"${PLUGIN_SKILLS_DIR}/" resolves outside the plugin root — the skills component is disabled for this plugin.` : `"${PLUGIN_SKILLS_DIR}" exists but is not a directory — the skills component is disabled for this plugin.`,
916
+ path: dir
917
+ });
918
+ return {
919
+ status: "invalid",
920
+ skills: [],
921
+ diagnostics
922
+ };
923
+ }
924
+ let entries;
925
+ try {
926
+ entries = await readdir(located.real);
927
+ } catch (err) {
928
+ diagnostics.push({
929
+ severity: "error",
930
+ code: "skills-dir-unreadable",
931
+ message: `Could not read "${PLUGIN_SKILLS_DIR}/": ${err instanceof Error ? err.message : String(err)}`,
932
+ path: located.real
933
+ });
934
+ return {
935
+ status: "invalid",
936
+ skills: [],
937
+ diagnostics
938
+ };
939
+ }
940
+ const skills = [];
941
+ for (const entry of entries.sort()) {
942
+ const child = join(located.real, entry);
943
+ const childDir = await resolvePackagePath(boundary, child, "directory");
944
+ if (!childDir.ok) {
945
+ if (childDir.reason === "escaped") diagnostics.push({
946
+ severity: "warning",
947
+ code: "skill-dir-escapes-root",
948
+ message: `Skipped "${PLUGIN_SKILLS_DIR}/${entry}" — it resolves outside the plugin root.`,
949
+ path: child
950
+ });
951
+ continue;
952
+ }
953
+ const skillFile = join(childDir.real, "SKILL.md");
954
+ const located2 = await resolvePackagePath(boundary, skillFile, "file");
955
+ if (!located2.ok) {
956
+ if (located2.reason === "escaped") diagnostics.push({
957
+ severity: "error",
958
+ code: "skill-escapes-root",
959
+ message: `Skipped skill "${entry}" — its SKILL.md resolves outside the plugin root.`,
960
+ path: skillFile
961
+ });
962
+ else if (located2.reason === "wrong-kind") diagnostics.push({
963
+ severity: "warning",
964
+ code: "skill-file-wrong-kind",
965
+ message: `Skipped "${PLUGIN_SKILLS_DIR}/${entry}" — its SKILL.md is not a regular file.`,
966
+ path: skillFile
967
+ });
968
+ continue;
969
+ }
970
+ let parsed;
971
+ try {
972
+ parsed = await parseSkillFile(located2.real, { source });
973
+ } catch (err) {
974
+ diagnostics.push({
975
+ severity: "error",
976
+ code: "skill-unreadable",
977
+ message: `Skipped skill "${entry}" — its SKILL.md could not be read: ${err instanceof Error ? err.message : String(err)}`,
978
+ path: located2.real
979
+ });
980
+ continue;
981
+ }
982
+ if (!parsed) {
983
+ diagnostics.push({
984
+ severity: "error",
985
+ code: "skill-invalid",
986
+ message: `Skipped skill "${entry}" — its SKILL.md is not a valid Agent Skill (missing description, or unparseable frontmatter).`,
987
+ path: located2.real
988
+ });
989
+ continue;
990
+ }
991
+ skills.push(stamp(parsed, {
992
+ pluginName,
993
+ namePrefix,
994
+ entry,
995
+ diagnostics
996
+ }));
997
+ }
998
+ return {
999
+ status: "loaded",
1000
+ skills,
1001
+ diagnostics
1002
+ };
1003
+ }
1004
+ /**
1005
+ * Attach plugin provenance and apply the optional name prefix.
1006
+ *
1007
+ * Provenance goes in `metadata` under the `zidane.` prefix the Agent Skills
1008
+ * spec reserves for client-specific keys (see `../skills/types.ts`), so the
1009
+ * skill stays spec-conformant while a host UI can still say which plugin
1010
+ * shipped it.
1011
+ */
1012
+ function stamp(skill, ctx) {
1013
+ const name = ctx.namePrefix ? `${ctx.namePrefix}-${skill.name}` : skill.name;
1014
+ if (name.length > SKILL_NAME_MAX) ctx.diagnostics.push({
1015
+ severity: "warning",
1016
+ code: "skill-name-too-long",
1017
+ message: `Prefixed skill name "${name}" exceeds the ${SKILL_NAME_MAX}-character spec limit. Loading anyway.`
1018
+ });
1019
+ return {
1020
+ ...skill,
1021
+ name,
1022
+ metadata: {
1023
+ ...skill.metadata,
1024
+ "zidane.plugin": ctx.pluginName
1025
+ }
1026
+ };
1027
+ }
1028
+ //#endregion
1029
+ //#region src/plugins/load.ts
1030
+ /** Fixed location of the plugin manifest. */
1031
+ const PLUGIN_MANIFEST_FILE = "plugin.json";
1032
+ /**
1033
+ * Default parent of every plugin's `${PLUGIN_DATA}` directory.
1034
+ *
1035
+ * Deliberately OUTSIDE any plugin root: the spec requires the data directory
1036
+ * to persist across plugin updates, and an update replaces the package tree.
1037
+ * Keyed by plugin name so two copies of the same plugin (project + user)
1038
+ * share state, which is what an author storing a cache or a token expects.
1039
+ *
1040
+ * Routed through `resolveStorageDirs` rather than hardcoding `~/.zidane` so it
1041
+ * honours `ZIDANE_STORAGE_DIR` and the XDG dirs like every other persisted
1042
+ * location — otherwise an XDG-mode install would scatter plugin data into a
1043
+ * `~/.zidane` nothing else in the app touches.
1044
+ *
1045
+ * NB: `resolveStorageDirs` does NOT read `ZIDANE_PREFIX`; it defaults `prefix`
1046
+ * to `.zidane`. Callers with a configured prefix must pass it, or plugin data
1047
+ * lands beside a prefix the rest of the install doesn't use.
1048
+ */
1049
+ function defaultPluginDataRoot(opts = {}) {
1050
+ return join(resolveStorageDirs(opts).dataDir, "plugin-data");
1051
+ }
1052
+ /**
1053
+ * Load an Agent Plugin from `dir`.
1054
+ *
1055
+ * @returns A {@link LoadedPlugin}. Check `ok` before trusting `manifest`;
1056
+ * check `components` to tell "the plugin ships no skills" (`absent`) from
1057
+ * "its skills were rejected" (`invalid`).
1058
+ */
1059
+ async function loadAgentPlugin(dir, opts = {}) {
1060
+ const diagnostics = [];
1061
+ const boundary = await resolvePluginBoundary(dir);
1062
+ if (!boundary) return rejected(resolve(dir), resolve(dir), [{
1063
+ severity: "error",
1064
+ code: "plugin-root-unreadable",
1065
+ message: `Plugin root "${dir}" does not exist or is not a directory.`,
1066
+ path: resolve(dir)
1067
+ }]);
1068
+ const manifestPath = join(boundary.root, PLUGIN_MANIFEST_FILE);
1069
+ const located = await resolvePackagePath(boundary, manifestPath, "file");
1070
+ if (!located.ok) return rejected(boundary.dir, boundary.root, [{
1071
+ severity: "error",
1072
+ code: located.reason === "escaped" ? "manifest-escapes-root" : "manifest-missing",
1073
+ message: located.reason === "escaped" ? `${PLUGIN_MANIFEST_FILE} resolves outside the plugin root.` : located.reason === "wrong-kind" ? `${PLUGIN_MANIFEST_FILE} exists but is not a regular file.` : `No ${PLUGIN_MANIFEST_FILE} at the plugin root — not an Agent Plugin.`,
1074
+ path: manifestPath
1075
+ }]);
1076
+ let text;
1077
+ try {
1078
+ text = await readFile(located.real, "utf8");
1079
+ } catch (err) {
1080
+ return rejected(boundary.dir, boundary.root, [{
1081
+ severity: "error",
1082
+ code: "manifest-unreadable",
1083
+ message: `Could not read ${PLUGIN_MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`,
1084
+ path: located.real
1085
+ }]);
1086
+ }
1087
+ const validated = parsePluginManifest(text);
1088
+ diagnostics.push(...validated.diagnostics);
1089
+ if (!validated.ok) return rejected(boundary.dir, boundary.root, diagnostics);
1090
+ const { manifest } = validated;
1091
+ const result = {
1092
+ ok: true,
1093
+ dir: boundary.dir,
1094
+ root: boundary.root,
1095
+ manifest,
1096
+ skills: [],
1097
+ mcpServers: [],
1098
+ components: {
1099
+ skills: "unsupported",
1100
+ mcp: "unsupported"
1101
+ },
1102
+ diagnostics
1103
+ };
1104
+ if (opts.skills !== false) await guardComponent(result, "skills", diagnostics, async () => {
1105
+ const skills = await loadPluginSkills({
1106
+ boundary,
1107
+ pluginName: manifest.name,
1108
+ ...opts.source !== void 0 ? { source: opts.source } : {},
1109
+ ...opts.namePrefix !== void 0 ? { namePrefix: opts.namePrefix } : {}
1110
+ });
1111
+ result.components.skills = skills.status;
1112
+ result.skills = skills.skills;
1113
+ diagnostics.push(...skills.diagnostics);
1114
+ });
1115
+ if (opts.mcp !== false) await guardComponent(result, "mcp", diagnostics, async () => {
1116
+ const mcp = await loadPluginMcp({
1117
+ boundary,
1118
+ pluginName: manifest.name,
1119
+ dataDir: opts.dataDir ?? join(opts.dataRoot ?? defaultPluginDataRoot(), manifest.name),
1120
+ specVersion: manifest.specVersion,
1121
+ ...opts.namePrefix !== void 0 ? { namePrefix: opts.namePrefix } : {}
1122
+ });
1123
+ result.components.mcp = mcp.status;
1124
+ result.mcpServers = mcp.servers;
1125
+ diagnostics.push(...mcp.diagnostics);
1126
+ });
1127
+ return result;
1128
+ }
1129
+ /**
1130
+ * Run one component-discovery pass, converting an unexpected throw into an
1131
+ * `invalid` component plus a diagnostic. Partial results the pass already
1132
+ * assigned are dropped, so a half-discovered component never looks loaded.
1133
+ *
1134
+ * No input reachable through this module's public API is known to trigger it:
1135
+ * both component loaders catch their own filesystem and parse failures, which
1136
+ * is exactly the property their tests assert. It is kept deliberately, not
1137
+ * abandoned — `loadAgentPlugin` promises never to throw for plugin-level
1138
+ * problems, and that promise should survive a future component type whose
1139
+ * loader forgets a `try`. The observable half IS tested: see the "never throws"
1140
+ * matrix in `test/plugins/load.test.ts`.
1141
+ */
1142
+ async function guardComponent(result, component, diagnostics, run) {
1143
+ try {
1144
+ await run();
1145
+ } catch (err) {
1146
+ result.components[component] = "invalid";
1147
+ if (component === "skills") result.skills = [];
1148
+ else result.mcpServers = [];
1149
+ diagnostics.push({
1150
+ severity: "error",
1151
+ code: `${component}-discovery-failed`,
1152
+ message: `Unexpected error while loading the ${component} component: ${err instanceof Error ? err.message : String(err)}`,
1153
+ path: result.root
1154
+ });
1155
+ }
1156
+ }
1157
+ function rejected(dir, root, diagnostics) {
1158
+ return {
1159
+ ok: false,
1160
+ dir,
1161
+ root,
1162
+ skills: [],
1163
+ mcpServers: [],
1164
+ components: {
1165
+ skills: "unsupported",
1166
+ mcp: "unsupported"
1167
+ },
1168
+ diagnostics
1169
+ };
1170
+ }
1171
+ //#endregion
1172
+ export { PLUGIN_MCP_FILE as a, validatePluginManifest as c, isSupportedAgentPluginsVersion as d, parseSchemaId as f, PLUGIN_SKILLS_DIR as i, SUPPORTED_AGENT_PLUGINS_VERSIONS as l, defaultPluginDataRoot as n, expandPluginVars as o, resolveStorageDirs as p, loadAgentPlugin as r, parsePluginManifest as s, PLUGIN_MANIFEST_FILE as t, agentPluginsSchemaId as u };
1173
+
1174
+ //# sourceMappingURL=plugins-2PMTVcp-.js.map