@orchid-labs/pluxx 0.1.14 → 0.1.15

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 (37) hide show
  1. package/dist/agent-translation-registry.d.ts +6 -0
  2. package/dist/agent-translation-registry.d.ts.map +1 -0
  3. package/dist/cli/agent.d.ts.map +1 -1
  4. package/dist/cli/behavioral.d.ts +2 -0
  5. package/dist/cli/behavioral.d.ts.map +1 -1
  6. package/dist/cli/discover-installed-mcp.d.ts +2 -1
  7. package/dist/cli/discover-installed-mcp.d.ts.map +1 -1
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/index.js +2433 -673
  10. package/dist/cli/init-from-mcp.d.ts +2 -0
  11. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  12. package/dist/cli/install.d.ts.map +1 -1
  13. package/dist/cli/lint.d.ts.map +1 -1
  14. package/dist/cli/migrate.d.ts.map +1 -1
  15. package/dist/command-translation-registry.d.ts +9 -0
  16. package/dist/command-translation-registry.d.ts.map +1 -0
  17. package/dist/commands.d.ts +20 -0
  18. package/dist/commands.d.ts.map +1 -1
  19. package/dist/generators/base.d.ts.map +1 -1
  20. package/dist/generators/claude-code/index.d.ts.map +1 -1
  21. package/dist/generators/codex/index.d.ts +1 -0
  22. package/dist/generators/codex/index.d.ts.map +1 -1
  23. package/dist/generators/cursor/index.d.ts.map +1 -1
  24. package/dist/generators/opencode/index.d.ts +1 -0
  25. package/dist/generators/opencode/index.d.ts.map +1 -1
  26. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  27. package/dist/hook-translation-registry.d.ts +10 -0
  28. package/dist/hook-translation-registry.d.ts.map +1 -1
  29. package/dist/index.js +164 -3
  30. package/dist/mcp-native-overrides.d.ts +14 -0
  31. package/dist/mcp-native-overrides.d.ts.map +1 -0
  32. package/dist/schema.d.ts +36 -36
  33. package/dist/skill-translation-registry.d.ts +8 -0
  34. package/dist/skill-translation-registry.d.ts.map +1 -0
  35. package/dist/skills.d.ts +24 -0
  36. package/dist/skills.d.ts.map +1 -1
  37. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -5055,6 +5055,262 @@ function parsePluginRootReference(value) {
5055
5055
  };
5056
5056
  }
5057
5057
 
5058
+ // src/user-config.ts
5059
+ var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5060
+ var PLACEHOLDER_SECRET_PATTERNS = [
5061
+ /\bdummy\b/i,
5062
+ /\bplaceholder\b/i,
5063
+ /\bexample\b/i,
5064
+ /\bchangeme\b/i,
5065
+ /\breplace[_ -]?me\b/i,
5066
+ /\byour[_ -]?(api[_ -]?)?key\b/i,
5067
+ /\bapi[_ -]?key[_ -]?here\b/i,
5068
+ /\btoken[_ -]?here\b/i
5069
+ ];
5070
+ function normalizeUserConfigKey(value) {
5071
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[._/\s]+/g, "-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
5072
+ }
5073
+ function humanizeUserConfigLabel(value) {
5074
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[._/]+/g, " ").replace(/-/g, " ").split(/\s+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
5075
+ }
5076
+ function defaultUserConfigEnvVar(key) {
5077
+ return key.replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
5078
+ }
5079
+ function extractEnvReference(value) {
5080
+ if (!value) return void 0;
5081
+ const match = value.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
5082
+ return match?.[1];
5083
+ }
5084
+ function isPlaceholderSecretValue(value) {
5085
+ if (typeof value !== "string") return false;
5086
+ const normalized = value.trim();
5087
+ if (normalized === "") return false;
5088
+ return PLACEHOLDER_SECRET_PATTERNS.some((pattern) => pattern.test(normalized));
5089
+ }
5090
+ function isRuntimePlatformManaged(config, target, server) {
5091
+ if (server.transport === "stdio") return false;
5092
+ if (server.auth?.type === "platform") {
5093
+ return true;
5094
+ }
5095
+ if (target === "claude-code") {
5096
+ return config.platforms?.["claude-code"]?.mcpAuth === "platform";
5097
+ }
5098
+ if (target === "cursor") {
5099
+ return config.platforms?.cursor?.mcpAuth === "platform";
5100
+ }
5101
+ return false;
5102
+ }
5103
+ function targetApplies(entry, target) {
5104
+ return !entry.targets || entry.targets.includes(target);
5105
+ }
5106
+ function dedupeUserConfigEntries(entries) {
5107
+ const deduped = [];
5108
+ const seenKeys = /* @__PURE__ */ new Set();
5109
+ const seenEnvVars = /* @__PURE__ */ new Set();
5110
+ for (const entry of entries) {
5111
+ const envVar = entry.envVar?.trim();
5112
+ if (seenKeys.has(entry.key)) continue;
5113
+ if (envVar && seenEnvVars.has(envVar)) continue;
5114
+ deduped.push(entry);
5115
+ seenKeys.add(entry.key);
5116
+ if (envVar) seenEnvVars.add(envVar);
5117
+ }
5118
+ return deduped;
5119
+ }
5120
+ function collectUserConfigEntries(config, platforms = config.targets) {
5121
+ const explicitEntries = (config.userConfig ?? []).filter((entry) => !entry.targets || entry.targets.some((target) => platforms.includes(target))).map((entry) => ({ ...entry, source: "explicit" }));
5122
+ const derivedEntries = [];
5123
+ for (const [serverName, server] of Object.entries(config.mcp ?? {})) {
5124
+ const applicableTargets = platforms.filter((target) => !isRuntimePlatformManaged(config, target, server));
5125
+ if (server.auth?.type && server.auth.type !== "none" && server.auth.type !== "platform" && ENV_VAR_NAME.test(server.auth.envVar) && applicableTargets.length > 0) {
5126
+ derivedEntries.push({
5127
+ key: normalizeUserConfigKey(server.auth.envVar),
5128
+ title: humanizeUserConfigLabel(server.auth.envVar),
5129
+ description: `Authentication credential for the ${serverName} MCP server.`,
5130
+ type: "secret",
5131
+ required: true,
5132
+ envVar: server.auth.envVar,
5133
+ targets: applicableTargets,
5134
+ source: "mcp-auth"
5135
+ });
5136
+ }
5137
+ if (server.transport === "stdio") {
5138
+ for (const [key, rawValue] of Object.entries(server.env ?? {})) {
5139
+ const envVar = extractEnvReference(rawValue) ?? (ENV_VAR_NAME.test(key) ? key : void 0);
5140
+ if (!envVar || !ENV_VAR_NAME.test(envVar)) continue;
5141
+ if (extractEnvReference(rawValue) === void 0 && rawValue !== "") {
5142
+ continue;
5143
+ }
5144
+ derivedEntries.push({
5145
+ key: normalizeUserConfigKey(envVar),
5146
+ title: humanizeUserConfigLabel(envVar),
5147
+ description: `Environment value required to launch the ${serverName} stdio MCP server.`,
5148
+ type: "secret",
5149
+ required: true,
5150
+ envVar,
5151
+ targets: applicableTargets.length > 0 ? applicableTargets : platforms,
5152
+ source: "mcp-env"
5153
+ });
5154
+ }
5155
+ }
5156
+ }
5157
+ return dedupeUserConfigEntries([
5158
+ ...explicitEntries,
5159
+ ...derivedEntries
5160
+ ]);
5161
+ }
5162
+ function resolveUserConfigEntriesForTarget(entries, target) {
5163
+ return entries.filter(({ field }) => targetApplies(field, target));
5164
+ }
5165
+ function buildUserConfigEnvMap(entries) {
5166
+ const env = {};
5167
+ for (const entry of entries) {
5168
+ if (!entry.envVar) continue;
5169
+ env[entry.envVar] = String(entry.value);
5170
+ }
5171
+ return env;
5172
+ }
5173
+ function buildUserConfigValueMap(entries) {
5174
+ const values = {};
5175
+ for (const entry of entries) {
5176
+ values[entry.field.key] = entry.value;
5177
+ }
5178
+ return values;
5179
+ }
5180
+
5181
+ // src/mcp-native-overrides.ts
5182
+ function asRecord(value) {
5183
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
5184
+ return value;
5185
+ }
5186
+ function cloneRecord(value) {
5187
+ return JSON.parse(JSON.stringify(value));
5188
+ }
5189
+ function firstString(...values) {
5190
+ for (const value of values) {
5191
+ if (typeof value === "string" && value.trim()) return value.trim();
5192
+ }
5193
+ return void 0;
5194
+ }
5195
+ function readStringRecord(value) {
5196
+ const record = asRecord(value);
5197
+ if (!record) return void 0;
5198
+ const stringRecord = Object.fromEntries(
5199
+ Object.entries(record).filter(([, entryValue]) => typeof entryValue === "string")
5200
+ );
5201
+ return Object.keys(stringRecord).length > 0 ? stringRecord : void 0;
5202
+ }
5203
+ function extractNativeMcpAuthConfig(cfg) {
5204
+ const preserved = {};
5205
+ const auth = asRecord(cfg.auth);
5206
+ if (auth) preserved.auth = cloneRecord(auth);
5207
+ const bearerTokenEnv = firstString(cfg.bearer_token_env_var, cfg.bearerTokenEnvVar);
5208
+ if (typeof cfg.bearer_token_env_var === "string") {
5209
+ preserved.bearer_token_env_var = cfg.bearer_token_env_var;
5210
+ } else if (typeof cfg.bearerTokenEnvVar === "string" && bearerTokenEnv) {
5211
+ preserved.bearerTokenEnvVar = bearerTokenEnv;
5212
+ }
5213
+ for (const key of ["env_http_headers", "envHttpHeaders", "headers", "http_headers", "httpHeaders"]) {
5214
+ const value = asRecord(cfg[key]);
5215
+ if (value) {
5216
+ preserved[key] = cloneRecord(value);
5217
+ }
5218
+ }
5219
+ return Object.keys(preserved).length > 0 ? preserved : void 0;
5220
+ }
5221
+ function buildNativeMcpPlatformOverrides(platform, servers) {
5222
+ const preservedServers = {};
5223
+ for (const [serverName, rawConfig] of Object.entries(servers)) {
5224
+ const cfg = asRecord(rawConfig);
5225
+ if (!cfg) continue;
5226
+ const nativeAuth = extractNativeMcpAuthConfig(cfg);
5227
+ if (!nativeAuth) continue;
5228
+ preservedServers[serverName] = nativeAuth;
5229
+ }
5230
+ if (Object.keys(preservedServers).length === 0) return void 0;
5231
+ return {
5232
+ [platform]: {
5233
+ mcpServers: preservedServers
5234
+ }
5235
+ };
5236
+ }
5237
+ function extractEnvVar(value) {
5238
+ const match = value.match(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/);
5239
+ return match?.[1] ?? match?.[2];
5240
+ }
5241
+ function envVarToKey(envVar) {
5242
+ return envVar.toLowerCase().replace(/_/g, "-");
5243
+ }
5244
+ function getNativeMcpServerOverride(config, platform, serverName) {
5245
+ const platformConfig = asRecord(config.platforms?.[platform]);
5246
+ const mcpServers = asRecord(platformConfig?.mcpServers);
5247
+ const serverOverride = asRecord(mcpServers?.[serverName]);
5248
+ return serverOverride ? cloneRecord(serverOverride) : void 0;
5249
+ }
5250
+ function getNativeJsonHeadersOverride(config, platform, serverName) {
5251
+ const override = getNativeMcpServerOverride(config, platform, serverName);
5252
+ return readStringRecord(override?.headers);
5253
+ }
5254
+ function getNativeCodexMcpEntryOverride(config, serverName) {
5255
+ const override = getNativeMcpServerOverride(config, "codex", serverName);
5256
+ if (!override) return void 0;
5257
+ const entry = {};
5258
+ const bearerTokenEnv = firstString(override.bearer_token_env_var, override.bearerTokenEnvVar);
5259
+ if (bearerTokenEnv) entry.bearer_token_env_var = bearerTokenEnv;
5260
+ const envHttpHeaders = readStringRecord(override.env_http_headers ?? override.envHttpHeaders);
5261
+ if (envHttpHeaders) entry.env_http_headers = envHttpHeaders;
5262
+ const httpHeaders = readStringRecord(override.http_headers ?? override.httpHeaders);
5263
+ if (httpHeaders) entry.http_headers = httpHeaders;
5264
+ const auth = asRecord(override.auth);
5265
+ if (auth) entry.auth = cloneRecord(auth);
5266
+ return Object.keys(entry).length > 0 ? entry : void 0;
5267
+ }
5268
+ function collectNativeMcpAuthUserConfigEntries(config, platforms, existingFields) {
5269
+ const next = [];
5270
+ const seenKeys = new Set(existingFields.map((field) => field.key));
5271
+ const seenEnvVars = new Set(
5272
+ existingFields.map((field) => field.envVar ?? defaultUserConfigEnvVar(field.key))
5273
+ );
5274
+ const pushDerived = (envVar, platform, serverName) => {
5275
+ if (!envVar || seenEnvVars.has(envVar)) return;
5276
+ const key = envVarToKey(envVar);
5277
+ if (seenKeys.has(key)) return;
5278
+ seenEnvVars.add(envVar);
5279
+ seenKeys.add(key);
5280
+ next.push({
5281
+ key,
5282
+ title: envVar,
5283
+ description: `Derived from native ${platform} MCP auth for ${serverName}.`,
5284
+ type: "secret",
5285
+ required: true,
5286
+ envVar,
5287
+ targets: [platform]
5288
+ });
5289
+ };
5290
+ for (const platform of platforms) {
5291
+ const platformConfig = asRecord(config.platforms?.[platform]);
5292
+ const mcpServers = asRecord(platformConfig?.mcpServers);
5293
+ if (!mcpServers) continue;
5294
+ for (const [serverName, rawOverride] of Object.entries(mcpServers)) {
5295
+ const override = asRecord(rawOverride);
5296
+ if (!override) continue;
5297
+ const bearerTokenEnv = firstString(override.bearer_token_env_var, override.bearerTokenEnvVar);
5298
+ if (bearerTokenEnv) pushDerived(bearerTokenEnv, platform, serverName);
5299
+ for (const record of [
5300
+ readStringRecord(override.env_http_headers ?? override.envHttpHeaders),
5301
+ readStringRecord(override.headers),
5302
+ readStringRecord(override.http_headers ?? override.httpHeaders)
5303
+ ]) {
5304
+ for (const value of Object.values(record ?? {})) {
5305
+ const envVar = extractEnvVar(value) ?? value;
5306
+ pushDerived(envVar, platform, serverName);
5307
+ }
5308
+ }
5309
+ }
5310
+ }
5311
+ return next;
5312
+ }
5313
+
5058
5314
  // src/generators/base.ts
5059
5315
  var Generator = class {
5060
5316
  constructor(config, rootDir) {
@@ -5162,6 +5418,10 @@ var Generator = class {
5162
5418
  entry.headers = headers;
5163
5419
  }
5164
5420
  }
5421
+ const nativeHeaders = getNativeJsonHeadersOverride(this.config, this.platform, name);
5422
+ if (nativeHeaders && this.getMcpAuthMode() !== "platform" && remoteServer.auth?.type !== "platform") {
5423
+ entry.headers = nativeHeaders;
5424
+ }
5165
5425
  if (transformRemoteEntry) {
5166
5426
  entry = transformRemoteEntry({ name, server: remoteServer, entry });
5167
5427
  }
@@ -5211,9 +5471,40 @@ var Generator = class {
5211
5471
  // src/generators/shared/claude-family.ts
5212
5472
  import { resolve as resolve7 } from "path";
5213
5473
 
5474
+ // src/hook-events.ts
5475
+ var CURSOR_SUPPORTED_HOOK_EVENTS = [
5476
+ "sessionStart",
5477
+ "sessionEnd",
5478
+ "preToolUse",
5479
+ "postToolUse",
5480
+ "postToolUseFailure",
5481
+ "subagentStart",
5482
+ "subagentStop",
5483
+ "beforeShellExecution",
5484
+ "afterShellExecution",
5485
+ "beforeMCPExecution",
5486
+ "afterMCPExecution",
5487
+ "beforeReadFile",
5488
+ "afterFileEdit",
5489
+ "beforeSubmitPrompt",
5490
+ "preCompact",
5491
+ "stop",
5492
+ "afterAgentResponse",
5493
+ "afterAgentThought",
5494
+ "beforeTabFileRead",
5495
+ "afterTabFileEdit"
5496
+ ];
5497
+ var PASCAL_CASE_HOOK_ALIASES = {
5498
+ beforeSubmitPrompt: "UserPromptSubmit"
5499
+ };
5500
+ function mapHookEventToPascalCase(event) {
5501
+ return PASCAL_CASE_HOOK_ALIASES[event] ?? event.charAt(0).toUpperCase() + event.slice(1);
5502
+ }
5503
+
5214
5504
  // src/hook-translation-registry.ts
5215
5505
  var HOOK_PLATFORM_REGISTRY = {
5216
5506
  "claude-code": {
5507
+ supportedTypes: ["command", "http", "mcp_tool", "prompt", "agent"],
5217
5508
  fields: {
5218
5509
  prompt: {
5219
5510
  mode: "preserve",
@@ -5237,6 +5528,9 @@ var HOOK_PLATFORM_REGISTRY = {
5237
5528
  }
5238
5529
  },
5239
5530
  cursor: {
5531
+ supportedTypes: ["command", "prompt"],
5532
+ supportedEvents: CURSOR_SUPPORTED_HOOK_EVENTS,
5533
+ unsupportedEventReason: `Cursor currently documents only ${CURSOR_SUPPORTED_HOOK_EVENTS.join(", ")} for hook configuration.`,
5240
5534
  fields: {
5241
5535
  prompt: { mode: "preserve" },
5242
5536
  matcher: { mode: "preserve" },
@@ -5245,6 +5539,7 @@ var HOOK_PLATFORM_REGISTRY = {
5245
5539
  }
5246
5540
  },
5247
5541
  codex: {
5542
+ supportedTypes: ["command"],
5248
5543
  supportedEvents: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
5249
5544
  unsupportedEventReason: "Codex currently documents only SessionStart, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, and Stop for hook configuration.",
5250
5545
  fields: {
@@ -5255,6 +5550,7 @@ var HOOK_PLATFORM_REGISTRY = {
5255
5550
  }
5256
5551
  },
5257
5552
  opencode: {
5553
+ supportedTypes: ["command"],
5258
5554
  fields: {
5259
5555
  prompt: { mode: "drop" },
5260
5556
  matcher: { mode: "preserve" },
@@ -5322,6 +5618,9 @@ var HOOK_PLATFORM_REGISTRY = {
5322
5618
  function getSupportedHookEvents(platform) {
5323
5619
  return HOOK_PLATFORM_REGISTRY[platform]?.supportedEvents ?? [];
5324
5620
  }
5621
+ function getSupportedHookTypes(platform) {
5622
+ return HOOK_PLATFORM_REGISTRY[platform]?.supportedTypes ?? [];
5623
+ }
5325
5624
  function getUnsupportedHookEventReason(platform) {
5326
5625
  return HOOK_PLATFORM_REGISTRY[platform]?.unsupportedEventReason ?? null;
5327
5626
  }
@@ -5330,6 +5629,11 @@ function isHookEventSupported(platform, event) {
5330
5629
  if (supportedEvents.length === 0) return true;
5331
5630
  return supportedEvents.includes(event);
5332
5631
  }
5632
+ function isHookTypeSupported(platform, type) {
5633
+ const supportedTypes = getSupportedHookTypes(platform);
5634
+ if (supportedTypes.length === 0) return true;
5635
+ return supportedTypes.includes(type);
5636
+ }
5333
5637
  function getHookFieldCapability(platform, field) {
5334
5638
  return HOOK_PLATFORM_REGISTRY[platform]?.fields[field] ?? { mode: "drop" };
5335
5639
  }
@@ -5345,6 +5649,77 @@ function isHookFieldPreserved(platform, field, event) {
5345
5649
  function getHookFieldSupportedEvents(platform, field) {
5346
5650
  return getHookFieldCapability(platform, field).supportedEvents ?? [];
5347
5651
  }
5652
+ function getHookTypeTranslationIssue(platform, type) {
5653
+ if (platform === "cursor") {
5654
+ return {
5655
+ code: "cursor-hook-type-unsupported",
5656
+ message: `Cursor does not document hook type "${type}". Pluxx currently preserves only command and prompt hooks on the Cursor hook surface.`
5657
+ };
5658
+ }
5659
+ if (platform === "codex") {
5660
+ return {
5661
+ code: "codex-hook-type-drop",
5662
+ message: `Codex currently bundles only command-hook entries from Pluxx. ${type} hooks will be dropped from the generated Codex bundle.`
5663
+ };
5664
+ }
5665
+ if (platform === "opencode") {
5666
+ return {
5667
+ code: "opencode-hook-type-drop",
5668
+ message: `The current OpenCode runtime wrapper only emits command hooks. ${type} hooks will be dropped from the generated OpenCode plugin.`
5669
+ };
5670
+ }
5671
+ return null;
5672
+ }
5673
+ function getPromptHookTranslationIssue(platform, event) {
5674
+ if (platform === "claude-code") {
5675
+ if (event && isHookFieldPreserved(platform, "prompt", event)) return null;
5676
+ const supportedEvents = getHookFieldSupportedEvents(platform, "prompt");
5677
+ return {
5678
+ code: "claude-prompt-hook-degrade",
5679
+ message: `Claude currently preserves prompt hooks only on ${supportedEvents.join(", ")}. Prompt hooks on ${event ?? "other events"} will be dropped from generated Claude output.`
5680
+ };
5681
+ }
5682
+ if (platform === "codex") {
5683
+ return {
5684
+ code: "codex-prompt-hook-drop",
5685
+ message: "Codex currently receives only command-hook entries from Pluxx. Prompt hooks will be dropped from the generated Codex bundle."
5686
+ };
5687
+ }
5688
+ if (platform === "opencode") {
5689
+ return {
5690
+ code: "opencode-prompt-hook-drop",
5691
+ message: "The current OpenCode runtime wrapper only emits command hooks. Prompt hooks will be dropped from the generated OpenCode plugin."
5692
+ };
5693
+ }
5694
+ return null;
5695
+ }
5696
+ function getHookFieldTranslationIssue(platform, field) {
5697
+ if (platform === "claude-code" && field === "failClosed") {
5698
+ return {
5699
+ code: "claude-hook-failclosed-degrade",
5700
+ message: "Claude hook entries currently drop `failClosed` in generated output. Keep this behavior host-specific or verify the generated hook bundle carefully."
5701
+ };
5702
+ }
5703
+ if (platform === "claude-code" && field === "loop_limit") {
5704
+ return {
5705
+ code: "claude-hook-loop-limit-degrade",
5706
+ message: "Claude outputs currently drop `loop_limit`. Recursive hook protection is not preserved there today."
5707
+ };
5708
+ }
5709
+ if (platform === "codex" && field === "loop_limit") {
5710
+ return {
5711
+ code: "codex-hook-loop-limit-drop",
5712
+ message: "Codex hook companions currently drop `loop_limit`. Only command, matcher, timeout, and failClosed survive there today."
5713
+ };
5714
+ }
5715
+ if (platform === "opencode" && field === "loop_limit") {
5716
+ return {
5717
+ code: "opencode-hook-loop-limit-drop",
5718
+ message: "OpenCode runtime hooks currently drop `loop_limit`. Recursive hook protection is still Cursor-first in Pluxx."
5719
+ };
5720
+ }
5721
+ return null;
5722
+ }
5348
5723
 
5349
5724
  // src/generators/hooks-warning.ts
5350
5725
  function warnDroppedHookFields(platform, event, entries) {
@@ -5374,36 +5749,6 @@ function warnDroppedHookFields(platform, event, entries) {
5374
5749
  }
5375
5750
  }
5376
5751
 
5377
- // src/hook-events.ts
5378
- var CURSOR_SUPPORTED_HOOK_EVENTS = [
5379
- "sessionStart",
5380
- "sessionEnd",
5381
- "preToolUse",
5382
- "postToolUse",
5383
- "postToolUseFailure",
5384
- "subagentStart",
5385
- "subagentStop",
5386
- "beforeShellExecution",
5387
- "afterShellExecution",
5388
- "beforeMCPExecution",
5389
- "afterMCPExecution",
5390
- "beforeReadFile",
5391
- "afterFileEdit",
5392
- "beforeSubmitPrompt",
5393
- "preCompact",
5394
- "stop",
5395
- "afterAgentResponse",
5396
- "afterAgentThought",
5397
- "beforeTabFileRead",
5398
- "afterTabFileEdit"
5399
- ];
5400
- var PASCAL_CASE_HOOK_ALIASES = {
5401
- beforeSubmitPrompt: "UserPromptSubmit"
5402
- };
5403
- function mapHookEventToPascalCase(event) {
5404
- return PASCAL_CASE_HOOK_ALIASES[event] ?? event.charAt(0).toUpperCase() + event.slice(1);
5405
- }
5406
-
5407
5752
  // src/readiness.ts
5408
5753
  function getRuntimeReadinessPlan(readiness) {
5409
5754
  if (!readiness || readiness.dependencies.length === 0 && readiness.gates.length === 0) {
@@ -6144,7 +6489,10 @@ async function writeMcpConfig(config, platform, writeJson) {
6144
6489
  mcpServers[name] = entry;
6145
6490
  continue;
6146
6491
  }
6147
- if (server.auth?.type === "bearer" && server.auth.envVar) {
6492
+ const nativeHeaders = getNativeJsonHeadersOverride(config, platform, name);
6493
+ if (nativeHeaders) {
6494
+ entry.headers = nativeHeaders;
6495
+ } else if (server.auth?.type === "bearer" && server.auth.envVar) {
6148
6496
  entry.headers = {
6149
6497
  Authorization: `Bearer \${${server.auth.envVar}}`
6150
6498
  };
@@ -6338,7 +6686,7 @@ function defaultMapEventName(event) {
6338
6686
  }
6339
6687
 
6340
6688
  // src/generators/claude-code/index.ts
6341
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
6689
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync6, readdirSync as readdirSync4, writeFileSync } from "fs";
6342
6690
  import { basename as basename2, join as join2 } from "path";
6343
6691
 
6344
6692
  // src/delegation.ts
@@ -6380,10 +6728,204 @@ function asMap2(value) {
6380
6728
  return value;
6381
6729
  }
6382
6730
 
6383
- // src/skills.ts
6731
+ // src/commands.ts
6384
6732
  import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
6385
6733
  import { relative as relative2, resolve as resolve8 } from "path";
6386
6734
  function unquote(value) {
6735
+ const trimmed = value.trim();
6736
+ if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
6737
+ return { value: trimmed.slice(1, -1), quoted: true };
6738
+ }
6739
+ return { value: trimmed, quoted: false };
6740
+ }
6741
+ function firstHeading2(content) {
6742
+ const lines = content.split(/\r?\n/);
6743
+ for (const line of lines) {
6744
+ const match = line.match(/^#\s+(.*)$/);
6745
+ if (match?.[1]?.trim()) return match[1].trim();
6746
+ }
6747
+ return void 0;
6748
+ }
6749
+ function splitMarkdownFrontmatter2(content) {
6750
+ const lines = content.split(/\r?\n/);
6751
+ if (lines[0]?.trim() !== "---") {
6752
+ return {
6753
+ frontmatterLines: [],
6754
+ body: content
6755
+ };
6756
+ }
6757
+ let endIndex = -1;
6758
+ for (let i = 1; i < lines.length; i += 1) {
6759
+ if (lines[i].trim() === "---") {
6760
+ endIndex = i;
6761
+ break;
6762
+ }
6763
+ }
6764
+ if (endIndex === -1) {
6765
+ return {
6766
+ frontmatterLines: [],
6767
+ body: content
6768
+ };
6769
+ }
6770
+ return {
6771
+ frontmatterLines: lines.slice(1, endIndex),
6772
+ body: lines.slice(endIndex + 1).join("\n")
6773
+ };
6774
+ }
6775
+ function stripYamlScalar(value) {
6776
+ return unquote(value).value.trim();
6777
+ }
6778
+ function parseFrontmatterFields(frontmatterLines) {
6779
+ const fields = /* @__PURE__ */ new Map();
6780
+ for (const line of frontmatterLines) {
6781
+ const trimmed = line.trim();
6782
+ if (!trimmed || trimmed.startsWith("#")) continue;
6783
+ const match = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(trimmed);
6784
+ if (!match) continue;
6785
+ const key = match[1];
6786
+ const rawValue = match[2];
6787
+ const parsed = unquote(rawValue);
6788
+ fields.set(key, {
6789
+ key,
6790
+ value: parsed.value,
6791
+ rawValue: rawValue.trim(),
6792
+ quoted: parsed.quoted
6793
+ });
6794
+ }
6795
+ return fields;
6796
+ }
6797
+ function parseCommandFrontmatterDescription(frontmatterLines) {
6798
+ for (const line of frontmatterLines) {
6799
+ const match = /^description:\s*(.+)\s*$/i.exec(line.trim());
6800
+ if (match?.[1]) {
6801
+ return stripYamlScalar(match[1]);
6802
+ }
6803
+ }
6804
+ return void 0;
6805
+ }
6806
+ function parseCommandFrontmatterString(frontmatterFields, key) {
6807
+ return frontmatterFields.get(key)?.value;
6808
+ }
6809
+ function parseCommandFrontmatterBoolean(frontmatterFields, key) {
6810
+ const value = parseCommandFrontmatterString(frontmatterFields, key);
6811
+ if (!value) return void 0;
6812
+ if (/^true$/i.test(value)) return true;
6813
+ if (/^false$/i.test(value)) return false;
6814
+ return void 0;
6815
+ }
6816
+ function parseInlineStringArray(rawValue) {
6817
+ const trimmed = rawValue.trim();
6818
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
6819
+ const inner = trimmed.slice(1, -1).trim();
6820
+ if (!inner) return [];
6821
+ return inner.split(",").map((part) => stripYamlScalar(part)).map((part) => part.trim()).filter(Boolean);
6822
+ }
6823
+ function parseCommandFrontmatterStringArray(frontmatterLines, frontmatterFields, key) {
6824
+ const rawValue = frontmatterFields.get(key)?.rawValue;
6825
+ if (rawValue) {
6826
+ const inlineArray = parseInlineStringArray(rawValue);
6827
+ if (inlineArray.length > 0) return inlineArray;
6828
+ if (rawValue.trim()) return [stripYamlScalar(rawValue)];
6829
+ }
6830
+ const lineIndex = frontmatterLines.findIndex((line) => new RegExp(`^${key}:\\s*$`, "i").test(line.trim()));
6831
+ if (lineIndex === -1) return [];
6832
+ const values = [];
6833
+ for (let index = lineIndex + 1; index < frontmatterLines.length; index += 1) {
6834
+ const itemMatch = /^\s*-\s+(.+)$/.exec(frontmatterLines[index]);
6835
+ if (!itemMatch?.[1]) break;
6836
+ const value = stripYamlScalar(itemMatch[1]).trim();
6837
+ if (value) values.push(value);
6838
+ }
6839
+ return values;
6840
+ }
6841
+ function inferCommandSkillLinks(body) {
6842
+ const skills = /* @__PURE__ */ new Set();
6843
+ for (const match of body.matchAll(/Use the `([^`]+)` skill\./g)) {
6844
+ const skill = match[1]?.trim();
6845
+ if (skill) skills.add(skill);
6846
+ }
6847
+ return [...skills];
6848
+ }
6849
+ function walkMarkdownFiles2(dir) {
6850
+ const entries = readdirSync2(dir);
6851
+ const files = [];
6852
+ for (const entry of entries) {
6853
+ const fullPath = resolve8(dir, entry);
6854
+ const stat = statSync2(fullPath);
6855
+ if (stat.isDirectory()) {
6856
+ files.push(...walkMarkdownFiles2(fullPath));
6857
+ continue;
6858
+ }
6859
+ if (stat.isFile() && entry.endsWith(".md")) {
6860
+ files.push(fullPath);
6861
+ }
6862
+ }
6863
+ return files;
6864
+ }
6865
+ function readCanonicalCommandFiles(commandsDir) {
6866
+ if (!commandsDir || !existsSync7(commandsDir)) return [];
6867
+ return walkMarkdownFiles2(commandsDir).sort((a, b) => a.localeCompare(b)).map((filePath) => {
6868
+ const content = readFileSync4(filePath, "utf-8");
6869
+ const { frontmatterLines, body } = splitMarkdownFrontmatter2(content);
6870
+ const frontmatterFields = parseFrontmatterFields(frontmatterLines);
6871
+ const relativeStem = relative2(commandsDir, filePath).replace(/\\/g, "/").replace(/\.md$/i, "");
6872
+ const commandId = relativeStem.replace(/\//g, "-").toLowerCase();
6873
+ const title = parseCommandFrontmatterString(frontmatterFields, "title") ?? firstHeading2(body) ?? commandId;
6874
+ const skills = [
6875
+ .../* @__PURE__ */ new Set([
6876
+ ...parseCommandFrontmatterStringArray(frontmatterLines, frontmatterFields, "skills"),
6877
+ ...(() => {
6878
+ const skill = parseCommandFrontmatterString(frontmatterFields, "skill");
6879
+ return skill ? [skill] : [];
6880
+ })(),
6881
+ ...inferCommandSkillLinks(body)
6882
+ ])
6883
+ ];
6884
+ return {
6885
+ filePath,
6886
+ relativeStem,
6887
+ commandId,
6888
+ frontmatterLines,
6889
+ frontmatterFields,
6890
+ title,
6891
+ description: parseCommandFrontmatterDescription(frontmatterLines),
6892
+ whenToUse: parseCommandFrontmatterString(frontmatterFields, "when_to_use"),
6893
+ argumentHint: parseCommandFrontmatterString(frontmatterFields, "argument-hint"),
6894
+ arguments: parseCommandFrontmatterStringArray(frontmatterLines, frontmatterFields, "arguments"),
6895
+ examples: parseCommandFrontmatterStringArray(frontmatterLines, frontmatterFields, "examples"),
6896
+ ...skills[0] ? { skill: skills[0] } : {},
6897
+ skills,
6898
+ agent: parseCommandFrontmatterString(frontmatterFields, "agent"),
6899
+ subtask: parseCommandFrontmatterBoolean(frontmatterFields, "subtask"),
6900
+ model: parseCommandFrontmatterString(frontmatterFields, "model"),
6901
+ context: parseCommandFrontmatterString(frontmatterFields, "context"),
6902
+ body: body.trim()
6903
+ };
6904
+ });
6905
+ }
6906
+ function getCanonicalCommandMetadata(command2) {
6907
+ return {
6908
+ commandId: command2.commandId,
6909
+ title: command2.title,
6910
+ ...command2.description ? { description: command2.description } : {},
6911
+ ...command2.whenToUse ? { whenToUse: command2.whenToUse } : {},
6912
+ ...command2.argumentHint ? { argumentHint: command2.argumentHint } : {},
6913
+ ...command2.arguments.length > 0 ? { arguments: [...command2.arguments] } : { arguments: [] },
6914
+ ...command2.examples.length > 0 ? { examples: [...command2.examples] } : { examples: [] },
6915
+ ...command2.skill ? { skill: command2.skill } : {},
6916
+ skills: [...command2.skills],
6917
+ ...command2.agent ? { agent: command2.agent } : {},
6918
+ ...typeof command2.subtask === "boolean" ? { subtask: command2.subtask } : {},
6919
+ ...command2.model ? { model: command2.model } : {},
6920
+ ...command2.context ? { context: command2.context } : {},
6921
+ template: command2.body
6922
+ };
6923
+ }
6924
+
6925
+ // src/skills.ts
6926
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
6927
+ import { relative as relative3, resolve as resolve9 } from "path";
6928
+ function unquote2(value) {
6387
6929
  const trimmed = value.trim();
6388
6930
  if (trimmed.length >= 2) {
6389
6931
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
@@ -6392,14 +6934,14 @@ function unquote(value) {
6392
6934
  }
6393
6935
  return { value: trimmed, quoted: false };
6394
6936
  }
6395
- function firstHeading2(content) {
6937
+ function firstHeading3(content) {
6396
6938
  for (const line of content.split(/\r?\n/)) {
6397
6939
  const match = /^#\s+(.+)$/.exec(line.trim());
6398
6940
  if (match?.[1]?.trim()) return match[1].trim();
6399
6941
  }
6400
6942
  return void 0;
6401
6943
  }
6402
- function splitMarkdownFrontmatter2(content) {
6944
+ function splitMarkdownFrontmatter3(content) {
6403
6945
  const lines = content.split(/\r?\n/);
6404
6946
  if (lines[0]?.trim() !== "---") {
6405
6947
  return {
@@ -6428,7 +6970,7 @@ function splitMarkdownFrontmatter2(content) {
6428
6970
  body: lines.slice(endIndex + 1).join("\n")
6429
6971
  };
6430
6972
  }
6431
- function parseFrontmatterFields(frontmatterLines) {
6973
+ function parseFrontmatterFields2(frontmatterLines) {
6432
6974
  const fields = /* @__PURE__ */ new Map();
6433
6975
  for (const line of frontmatterLines) {
6434
6976
  const trimmed = line.trim();
@@ -6437,7 +6979,7 @@ function parseFrontmatterFields(frontmatterLines) {
6437
6979
  if (!match) continue;
6438
6980
  const key = match[1];
6439
6981
  const rawValue = match[2];
6440
- const parsed = unquote(rawValue);
6982
+ const parsed = unquote2(rawValue);
6441
6983
  fields.set(key, {
6442
6984
  key,
6443
6985
  value: parsed.value,
@@ -6453,7 +6995,7 @@ function parseAllowedTools(hasValidFrontmatter, frontmatterLines, frontmatterFie
6453
6995
  const inlineValue = inlineField?.rawValue.trim() ?? "";
6454
6996
  if (inlineValue) {
6455
6997
  const raw = inlineValue.startsWith("[") && inlineValue.endsWith("]") ? inlineValue.slice(1, -1) : inlineValue;
6456
- return raw.split(",").map((part) => unquote(part).value).map((part) => part.trim()).filter(Boolean);
6998
+ return raw.split(",").map((part) => unquote2(part).value).map((part) => part.trim()).filter(Boolean);
6457
6999
  }
6458
7000
  const lineIndex = frontmatterLines.findIndex((line) => /^allowed-tools:\s*$/i.test(line.trim()));
6459
7001
  if (lineIndex === -1) return [];
@@ -6461,7 +7003,7 @@ function parseAllowedTools(hasValidFrontmatter, frontmatterLines, frontmatterFie
6461
7003
  for (let index = lineIndex + 1; index < frontmatterLines.length; index += 1) {
6462
7004
  const itemMatch = /^\s*-\s+(.+)$/.exec(frontmatterLines[index]);
6463
7005
  if (!itemMatch?.[1]) break;
6464
- const value = unquote(itemMatch[1]).value.trim();
7006
+ const value = unquote2(itemMatch[1]).value.trim();
6465
7007
  if (value) tools.push(value);
6466
7008
  }
6467
7009
  return tools;
@@ -6472,17 +7014,17 @@ function parseBooleanField(frontmatterFields, key) {
6472
7014
  if (value === "false") return false;
6473
7015
  return void 0;
6474
7016
  }
6475
- function parseInlineStringArray(rawValue) {
7017
+ function parseInlineStringArray2(rawValue) {
6476
7018
  const trimmed = rawValue.trim();
6477
7019
  if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
6478
7020
  const inner = trimmed.slice(1, -1).trim();
6479
7021
  if (!inner) return [];
6480
- return inner.split(",").map((part) => unquote(part).value.trim()).filter(Boolean);
7022
+ return inner.split(",").map((part) => unquote2(part).value.trim()).filter(Boolean);
6481
7023
  }
6482
7024
  function parseStringArrayField(frontmatterFields, key) {
6483
7025
  const rawValue = frontmatterFields.get(key)?.rawValue;
6484
7026
  if (!rawValue) return [];
6485
- return parseInlineStringArray(rawValue);
7027
+ return parseInlineStringArray2(rawValue);
6486
7028
  }
6487
7029
  function parseJsonField(frontmatterFields, key) {
6488
7030
  const rawValue = frontmatterFields.get(key)?.rawValue?.trim();
@@ -6499,8 +7041,8 @@ function parseSkillMarkdown(content) {
6499
7041
  hasValidFrontmatter,
6500
7042
  frontmatterLines,
6501
7043
  body
6502
- } = splitMarkdownFrontmatter2(content);
6503
- const frontmatterFields = parseFrontmatterFields(frontmatterLines);
7044
+ } = splitMarkdownFrontmatter3(content);
7045
+ const frontmatterFields = parseFrontmatterFields2(frontmatterLines);
6504
7046
  return {
6505
7047
  hasValidFrontmatter,
6506
7048
  frontmatterLines,
@@ -6521,16 +7063,16 @@ function parseSkillMarkdown(content) {
6521
7063
  hooks: parseJsonField(frontmatterFields, "hooks"),
6522
7064
  paths: parseStringArrayField(frontmatterFields, "paths"),
6523
7065
  shell: frontmatterFields.get("shell")?.value,
6524
- firstHeading: firstHeading2(body)
7066
+ firstHeading: firstHeading3(body)
6525
7067
  };
6526
7068
  }
6527
7069
  function walkSkillFiles(skillsDir) {
6528
- if (!skillsDir || !existsSync7(skillsDir)) return [];
6529
- const entries = readdirSync2(skillsDir);
7070
+ if (!skillsDir || !existsSync8(skillsDir)) return [];
7071
+ const entries = readdirSync3(skillsDir);
6530
7072
  const files = [];
6531
7073
  for (const entry of entries) {
6532
- const fullPath = resolve8(skillsDir, entry);
6533
- const stat = statSync2(fullPath);
7074
+ const fullPath = resolve9(skillsDir, entry);
7075
+ const stat = statSync3(fullPath);
6534
7076
  if (stat.isDirectory()) {
6535
7077
  files.push(...walkSkillFiles(fullPath));
6536
7078
  continue;
@@ -6542,12 +7084,12 @@ function walkSkillFiles(skillsDir) {
6542
7084
  return files;
6543
7085
  }
6544
7086
  function readSkillMarkdownFile(filePath) {
6545
- return parseSkillMarkdown(readFileSync4(filePath, "utf-8"));
7087
+ return parseSkillMarkdown(readFileSync5(filePath, "utf-8"));
6546
7088
  }
6547
7089
  function readCanonicalSkillFiles(skillsDir) {
6548
- if (!skillsDir || !existsSync7(skillsDir)) return [];
7090
+ if (!skillsDir || !existsSync8(skillsDir)) return [];
6549
7091
  return walkSkillFiles(skillsDir).sort((a, b) => a.localeCompare(b)).map((filePath) => {
6550
- const relativeDir = relative2(skillsDir, filePath).replace(/\\/g, "/").replace(/\/SKILL\.md$/i, "");
7092
+ const relativeDir = relative3(skillsDir, filePath).replace(/\\/g, "/").replace(/\/SKILL\.md$/i, "");
6551
7093
  return {
6552
7094
  filePath,
6553
7095
  relativeDir,
@@ -6560,6 +7102,48 @@ function serializeSkillMarkdown(frontmatterLines, body) {
6560
7102
  return ["---", ...frontmatterLines, "---", body ? `
6561
7103
  ${body.replace(/^\n/, "")}` : ""].join("\n");
6562
7104
  }
7105
+ function walkSupportFiles(dir) {
7106
+ const entries = readdirSync3(dir, { withFileTypes: true });
7107
+ const files = [];
7108
+ for (const entry of entries) {
7109
+ const fullPath = resolve9(dir, entry.name);
7110
+ if (entry.isDirectory()) {
7111
+ files.push(...walkSupportFiles(fullPath));
7112
+ continue;
7113
+ }
7114
+ if (entry.isFile()) {
7115
+ files.push(fullPath);
7116
+ }
7117
+ }
7118
+ return files;
7119
+ }
7120
+ function getCanonicalSkillMetadata(skill) {
7121
+ const skillDir = resolve9(skill.filePath, "..");
7122
+ const supportPaths = walkSupportFiles(skillDir).filter((fullPath) => fullPath !== skill.filePath).map((fullPath) => relative3(skillDir, fullPath).replace(/\\/g, "/")).sort((a, b) => a.localeCompare(b));
7123
+ return {
7124
+ dirName: skill.dirName,
7125
+ title: skill.firstHeading ?? skill.name ?? skill.dirName,
7126
+ ...skill.description ? { description: skill.description } : {},
7127
+ ...skill.whenToUse ? { whenToUse: skill.whenToUse } : {},
7128
+ ...skill.argumentHint ? { argumentHint: skill.argumentHint } : {},
7129
+ arguments: [...skill.arguments],
7130
+ ...typeof skill.disableModelInvocation === "boolean" ? { disableModelInvocation: skill.disableModelInvocation } : {},
7131
+ ...typeof skill.userInvocable === "boolean" ? { userInvocable: skill.userInvocable } : {},
7132
+ allowedTools: [...skill.allowedTools],
7133
+ ...skill.model ? { model: skill.model } : {},
7134
+ ...skill.effort ? { effort: skill.effort } : {},
7135
+ ...skill.context ? { context: skill.context } : {},
7136
+ ...skill.agent ? { agent: skill.agent } : {},
7137
+ ...skill.hooks !== void 0 ? { hooks: skill.hooks } : {},
7138
+ paths: [...skill.paths],
7139
+ ...skill.shell ? { shell: skill.shell } : {},
7140
+ body: skill.body,
7141
+ supportPaths,
7142
+ helperScripts: supportPaths.filter((path) => path.startsWith("scripts/")),
7143
+ examplePaths: supportPaths.filter((path) => path.startsWith("examples/")),
7144
+ referencePaths: supportPaths.filter((path) => !path.startsWith("examples/") && !path.startsWith("scripts/"))
7145
+ };
7146
+ }
6563
7147
 
6564
7148
  // src/generators/claude-code/index.ts
6565
7149
  var ClaudeCodeGenerator = class extends Generator {
@@ -6592,8 +7176,8 @@ var ClaudeCodeGenerator = class extends Generator {
6592
7176
  const wrappedSkills = this.collectCommandWrappedSkills();
6593
7177
  for (const skill of collidingSkills) {
6594
7178
  const outputPath = join2(this.outDir, "skills", skill.dirName, "SKILL.md");
6595
- if (!existsSync8(outputPath)) continue;
6596
- const current = readFileSync5(outputPath, "utf-8");
7179
+ if (!existsSync9(outputPath)) continue;
7180
+ const current = readFileSync6(outputPath, "utf-8");
6597
7181
  const hiddenName = buildHiddenSkillName(skill.effectiveName);
6598
7182
  const rewritten = rewriteClaudeSkillVisibility(current, {
6599
7183
  nameOverride: hiddenName,
@@ -6606,8 +7190,8 @@ var ClaudeCodeGenerator = class extends Generator {
6606
7190
  for (const skill of wrappedSkills) {
6607
7191
  if (collidingSkills.some((entry) => entry.dirName === skill.dirName)) continue;
6608
7192
  const outputPath = join2(this.outDir, "skills", skill.dirName, "SKILL.md");
6609
- if (!existsSync8(outputPath)) continue;
6610
- const current = readFileSync5(outputPath, "utf-8");
7193
+ if (!existsSync9(outputPath)) continue;
7194
+ const current = readFileSync6(outputPath, "utf-8");
6611
7195
  const rewritten = rewriteClaudeSkillVisibility(current, {
6612
7196
  userInvocable: false
6613
7197
  });
@@ -6682,7 +7266,7 @@ ${bodyParts.join("\n").trim()}
6682
7266
  if (!this.config.commands) return [];
6683
7267
  const commandsSrc = this.resolveConfigPath(this.config.commands, "commands");
6684
7268
  const skillsSrc = this.resolveConfigPath(this.config.skills, "skills");
6685
- if (!existsSync8(commandsSrc) || !existsSync8(skillsSrc)) return [];
7269
+ if (!existsSync9(commandsSrc) || !existsSync9(skillsSrc)) return [];
6686
7270
  const commandNames = collectTopLevelCommandNames(commandsSrc);
6687
7271
  const collidingSkills = [];
6688
7272
  for (const skill of readCanonicalSkillFiles(skillsSrc)) {
@@ -6697,7 +7281,7 @@ ${bodyParts.join("\n").trim()}
6697
7281
  if (!this.config.commands) return [];
6698
7282
  const commandsSrc = this.resolveConfigPath(this.config.commands, "commands");
6699
7283
  const skillsSrc = this.resolveConfigPath(this.config.skills, "skills");
6700
- if (!existsSync8(commandsSrc) || !existsSync8(skillsSrc)) return [];
7284
+ if (!existsSync9(commandsSrc) || !existsSync9(skillsSrc)) return [];
6701
7285
  const referencedSkills = collectWrappedSkillNames(commandsSrc);
6702
7286
  if (referencedSkills.size === 0) return [];
6703
7287
  const wrappedSkills = [];
@@ -6712,7 +7296,7 @@ ${bodyParts.join("\n").trim()}
6712
7296
  };
6713
7297
  function collectTopLevelCommandNames(commandsRoot) {
6714
7298
  const commandNames = /* @__PURE__ */ new Set();
6715
- for (const entry of readdirSync3(commandsRoot, { withFileTypes: true })) {
7299
+ for (const entry of readdirSync4(commandsRoot, { withFileTypes: true })) {
6716
7300
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
6717
7301
  commandNames.add(basename2(entry.name, ".md"));
6718
7302
  }
@@ -6721,11 +7305,9 @@ function collectTopLevelCommandNames(commandsRoot) {
6721
7305
  }
6722
7306
  function collectWrappedSkillNames(commandsRoot) {
6723
7307
  const wrappedSkills = /* @__PURE__ */ new Set();
6724
- for (const entry of readdirSync3(commandsRoot, { withFileTypes: true })) {
6725
- if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
6726
- const content = readFileSync5(join2(commandsRoot, entry.name), "utf-8");
6727
- for (const match of content.matchAll(/Use the `([^`]+)` skill\./g)) {
6728
- const skillName = match[1]?.trim();
7308
+ for (const command2 of readCanonicalCommandFiles(commandsRoot)) {
7309
+ const metadata = getCanonicalCommandMetadata(command2);
7310
+ for (const skillName of metadata.skills) {
6729
7311
  if (skillName) wrappedSkills.add(skillName);
6730
7312
  }
6731
7313
  }
@@ -6811,6 +7393,70 @@ function asMap3(value) {
6811
7393
  return value;
6812
7394
  }
6813
7395
 
7396
+ // src/agent-translation-registry.ts
7397
+ var HAS_BOOLEAN = (value) => typeof value === "boolean";
7398
+ var AGENT_TRANSLATION_PROFILES = {
7399
+ cursor: {
7400
+ degradedFields: [
7401
+ { key: "mode", present: (metadata) => !!metadata.mode },
7402
+ { key: "hidden", present: (metadata) => metadata.hidden },
7403
+ { key: "model_reasoning_effort", present: (metadata) => !!metadata.modelReasoningEffort },
7404
+ { key: "sandbox_mode", present: (metadata) => !!metadata.sandboxMode },
7405
+ { key: "temperature", present: (metadata) => typeof metadata.temperature === "number" },
7406
+ { key: "steps", present: (metadata) => typeof metadata.steps === "number" },
7407
+ { key: "disable", present: (metadata) => HAS_BOOLEAN(metadata.disabled) },
7408
+ { key: "color", present: (metadata) => !!metadata.color },
7409
+ { key: "topP", present: (metadata) => typeof metadata.topP === "number" },
7410
+ { key: "skills", present: (metadata) => !!metadata.skills },
7411
+ { key: "memory", present: (metadata) => !!metadata.memory },
7412
+ { key: "background", present: (metadata) => HAS_BOOLEAN(metadata.background) },
7413
+ { key: "isolation", present: (metadata) => !!metadata.isolation },
7414
+ { key: "permission", present: (metadata) => !!metadata.permission },
7415
+ { key: "tools", present: (metadata) => metadata.tools !== void 0 }
7416
+ ],
7417
+ guidanceSurfaces: ["subagent framing", "generated notes"],
7418
+ message: (fields) => `Agent fields ${fields.map((field) => `"${field}"`).join(", ")} are not preserved as first-class Cursor agent frontmatter today. Pluxx translates that intent through subagent framing and generated notes instead.`
7419
+ },
7420
+ codex: {
7421
+ degradedFields: [
7422
+ { key: "mode", present: (metadata) => !!metadata.mode },
7423
+ { key: "hidden", present: (metadata) => metadata.hidden },
7424
+ { key: "temperature", present: (metadata) => typeof metadata.temperature === "number" },
7425
+ { key: "steps", present: (metadata) => typeof metadata.steps === "number" },
7426
+ { key: "disable", present: (metadata) => HAS_BOOLEAN(metadata.disabled) },
7427
+ { key: "color", present: (metadata) => !!metadata.color },
7428
+ { key: "topP", present: (metadata) => typeof metadata.topP === "number" },
7429
+ { key: "skills", present: (metadata) => !!metadata.skills },
7430
+ { key: "memory", present: (metadata) => !!metadata.memory },
7431
+ { key: "background", present: (metadata) => HAS_BOOLEAN(metadata.background) },
7432
+ { key: "isolation", present: (metadata) => !!metadata.isolation },
7433
+ { key: "permission", present: (metadata) => !!metadata.permission },
7434
+ { key: "tools", present: (metadata) => metadata.tools !== void 0 }
7435
+ ],
7436
+ guidanceSurfaces: ["developer instructions", "generated companion surfaces"],
7437
+ message: (fields) => `Agent fields ${fields.map((field) => `"${field}"`).join(", ")} are not native Codex TOML fields today. Pluxx keeps the specialist behavior, but translates that intent through developer instructions and companion surfaces instead.`
7438
+ },
7439
+ opencode: {
7440
+ degradedFields: [
7441
+ { key: "model_reasoning_effort", present: (metadata) => !!metadata.modelReasoningEffort },
7442
+ { key: "sandbox_mode", present: (metadata) => !!metadata.sandboxMode },
7443
+ { key: "skills", present: (metadata) => !!metadata.skills },
7444
+ { key: "memory", present: (metadata) => !!metadata.memory },
7445
+ { key: "background", present: (metadata) => HAS_BOOLEAN(metadata.background) },
7446
+ { key: "isolation", present: (metadata) => !!metadata.isolation }
7447
+ ],
7448
+ guidanceSurfaces: ["agent config notes", "runtime/developer guidance"],
7449
+ message: (fields) => `Agent fields ${fields.map((field) => `"${field}"`).join(", ")} are not native OpenCode agent config fields today. Pluxx keeps the specialist behavior, but translates that intent through agent config notes and surrounding runtime guidance instead.`
7450
+ }
7451
+ };
7452
+ function getTranslatedAgentFields(platform, metadata) {
7453
+ return AGENT_TRANSLATION_PROFILES[platform].degradedFields.filter((field) => field.present(metadata)).map((field) => field.key);
7454
+ }
7455
+ function getAgentTranslationMessage(platform, fields) {
7456
+ if (fields.length === 0) return null;
7457
+ return AGENT_TRANSLATION_PROFILES[platform].message(fields);
7458
+ }
7459
+
6814
7460
  // src/generators/cursor/index.ts
6815
7461
  var CursorGenerator = class extends Generator {
6816
7462
  platform = "cursor";
@@ -6901,6 +7547,12 @@ var CursorGenerator = class extends Generator {
6901
7547
  }
6902
7548
  for (const [event, entries] of Object.entries(this.config.hooks)) {
6903
7549
  if (!entries) continue;
7550
+ if (!isHookEventSupported("cursor", event)) {
7551
+ console.warn(
7552
+ `[pluxx] cursor generator dropped unsupported hook event "${event}". ${getUnsupportedHookEventReason("cursor") ?? `Supported: ${getSupportedHookEvents("cursor").join(", ")}`}`
7553
+ );
7554
+ continue;
7555
+ }
6904
7556
  const filteredEntries = entries.filter((entry) => {
6905
7557
  if (usesPlatformManagedAuth && entry.type !== "prompt" && entry.command?.includes("check-env.sh")) {
6906
7558
  return false;
@@ -6990,7 +7642,7 @@ var CursorGenerator = class extends Generator {
6990
7642
  frontmatter.push(`model: ${JSON.stringify(metadata.model)}`);
6991
7643
  }
6992
7644
  frontmatter.push("---");
6993
- const translatedNotes = buildCursorAgentTranslationNotes(agent.frontmatter);
7645
+ const translatedNotes = buildCursorAgentTranslationNotes(agent.frontmatter, metadata);
6994
7646
  const bodyParts = [
6995
7647
  ...translatedNotes,
6996
7648
  agent.body
@@ -7005,8 +7657,12 @@ ${bodyParts.join("\n\n").trim()}
7005
7657
  }
7006
7658
  }
7007
7659
  };
7008
- function buildCursorAgentTranslationNotes(frontmatter) {
7009
- return buildDelegationBehaviorNotes(frontmatter).map(
7660
+ function buildCursorAgentTranslationNotes(frontmatter, metadata) {
7661
+ const notes = [
7662
+ getAgentTranslationMessage("cursor", getTranslatedAgentFields("cursor", metadata)),
7663
+ ...buildDelegationBehaviorNotes(frontmatter)
7664
+ ].filter((note) => Boolean(note));
7665
+ return notes.map(
7010
7666
  (note) => `Cursor translation note: ${note.charAt(0).toLowerCase()}${note.slice(1)}`
7011
7667
  );
7012
7668
  }
@@ -7014,125 +7670,57 @@ function buildCursorAgentTranslationNotes(frontmatter) {
7014
7670
  // src/generators/codex/index.ts
7015
7671
  import { relative as relative4 } from "path";
7016
7672
 
7017
- // src/commands.ts
7018
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
7019
- import { relative as relative3, resolve as resolve9 } from "path";
7020
- function firstHeading3(content) {
7021
- const lines = content.split(/\r?\n/);
7022
- for (const line of lines) {
7023
- const match = line.match(/^#\s+(.*)$/);
7024
- if (match?.[1]?.trim()) return match[1].trim();
7025
- }
7026
- return void 0;
7027
- }
7028
- function splitMarkdownFrontmatter3(content) {
7029
- const lines = content.split(/\r?\n/);
7030
- if (lines[0]?.trim() !== "---") {
7031
- return {
7032
- frontmatterLines: [],
7033
- body: content
7034
- };
7035
- }
7036
- let endIndex = -1;
7037
- for (let i = 1; i < lines.length; i += 1) {
7038
- if (lines[i].trim() === "---") {
7039
- endIndex = i;
7040
- break;
7041
- }
7042
- }
7043
- if (endIndex === -1) {
7044
- return {
7045
- frontmatterLines: [],
7046
- body: content
7047
- };
7048
- }
7049
- return {
7050
- frontmatterLines: lines.slice(1, endIndex),
7051
- body: lines.slice(endIndex + 1).join("\n")
7052
- };
7673
+ // src/command-translation-registry.ts
7674
+ var CODEX_COMMAND_FIELD_DESCRIPTORS = [
7675
+ { label: "when_to_use", present: (metadata) => Boolean(metadata.whenToUse) },
7676
+ { label: "argument-hint", present: (metadata) => Boolean(metadata.argumentHint) },
7677
+ { label: "arguments", present: (metadata) => metadata.arguments.length > 0 },
7678
+ { label: "examples", present: (metadata) => metadata.examples.length > 0 },
7679
+ { label: "skill", present: (metadata) => Boolean(metadata.skill) },
7680
+ { label: "skills", present: (metadata) => metadata.skills.length > 0 },
7681
+ { label: "agent", present: (metadata) => Boolean(metadata.agent) },
7682
+ { label: "subtask", present: (metadata) => typeof metadata.subtask === "boolean" },
7683
+ { label: "model", present: (metadata) => Boolean(metadata.model) },
7684
+ { label: "context", present: (metadata) => Boolean(metadata.context) }
7685
+ ];
7686
+ function getCodexCommandGuidanceNote() {
7687
+ return "Codex does not currently document plugin-packaged slash-command parity. Pluxx keeps canonical command intent through AGENTS.md routing guidance and `.codex/commands.generated.json`.";
7053
7688
  }
7054
- function stripYamlScalar(value) {
7055
- const trimmed = value.trim();
7056
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
7057
- return trimmed.slice(1, -1).trim();
7058
- }
7059
- return trimmed;
7689
+ function getTranslatedCommandFields(platform, metadata) {
7690
+ if (platform !== "codex") return [];
7691
+ return CODEX_COMMAND_FIELD_DESCRIPTORS.filter((descriptor) => descriptor.present(metadata)).map((descriptor) => descriptor.label);
7060
7692
  }
7061
- function parseCommandFrontmatterDescription(frontmatterLines) {
7062
- for (const line of frontmatterLines) {
7063
- const match = /^description:\s*(.+)\s*$/i.exec(line.trim());
7064
- if (match?.[1]) {
7065
- return stripYamlScalar(match[1]);
7066
- }
7693
+ function getCommandTranslationMessage(platform, degradedFields) {
7694
+ if (degradedFields.length === 0) return void 0;
7695
+ if (platform === "codex") {
7696
+ return `Command fields ${degradedFields.map((field) => `"${field}"`).join(", ")} are not native Codex plugin slash-command fields today. ${getCodexCommandGuidanceNote()}`;
7067
7697
  }
7068
7698
  return void 0;
7069
7699
  }
7070
- function parseCommandFrontmatterString(frontmatterLines, key) {
7071
- const pattern = new RegExp(`^${key}:\\s*(.+)\\s*$`, "i");
7072
- for (const line of frontmatterLines) {
7073
- const match = pattern.exec(line.trim());
7074
- if (match?.[1]) {
7075
- return stripYamlScalar(match[1]);
7076
- }
7700
+ function buildCodexCommandRoutingEntry(metadata) {
7701
+ const distinctSkills = metadata.skills.filter((skillId, index, skills) => skills.indexOf(skillId) === index);
7702
+ const hasOnlyPrimarySkill = distinctSkills.length === 1 && distinctSkills[0] === metadata.skill;
7703
+ const routingBits = [
7704
+ metadata.argumentHint ? `arguments: ${metadata.argumentHint}` : null,
7705
+ metadata.skill ? `skill: ${metadata.skill}` : null,
7706
+ distinctSkills.length > 0 && !hasOnlyPrimarySkill ? `skills: ${distinctSkills.join(", ")}` : null,
7707
+ metadata.agent ? `agent: ${metadata.agent}` : null,
7708
+ typeof metadata.subtask === "boolean" ? `subtask: ${metadata.subtask ? "yes" : "no"}` : null,
7709
+ metadata.model ? `model: ${metadata.model}` : null
7710
+ ].filter(Boolean);
7711
+ const lines = [
7712
+ `- \`/${metadata.commandId}\` - ${metadata.description ?? metadata.title}${routingBits.length > 0 ? ` (${routingBits.join("; ")})` : ""}`
7713
+ ];
7714
+ if (metadata.whenToUse) {
7715
+ lines.push(` - When to use: ${metadata.whenToUse}`);
7077
7716
  }
7078
- return void 0;
7079
- }
7080
- function parseCommandFrontmatterBoolean(frontmatterLines, key) {
7081
- const value = parseCommandFrontmatterString(frontmatterLines, key);
7082
- if (!value) return void 0;
7083
- if (/^true$/i.test(value)) return true;
7084
- if (/^false$/i.test(value)) return false;
7085
- return void 0;
7086
- }
7087
- function walkMarkdownFiles2(dir) {
7088
- const entries = readdirSync4(dir);
7089
- const files = [];
7090
- for (const entry of entries) {
7091
- const fullPath = resolve9(dir, entry);
7092
- const stat = statSync3(fullPath);
7093
- if (stat.isDirectory()) {
7094
- files.push(...walkMarkdownFiles2(fullPath));
7095
- continue;
7096
- }
7097
- if (stat.isFile() && entry.endsWith(".md")) {
7098
- files.push(fullPath);
7099
- }
7717
+ if (metadata.examples.length > 0) {
7718
+ lines.push(` - Examples: ${metadata.examples.map((example) => `\`${example}\``).join(", ")}`);
7100
7719
  }
7101
- return files;
7102
- }
7103
- function readCanonicalCommandFiles(commandsDir) {
7104
- if (!commandsDir || !existsSync9(commandsDir)) return [];
7105
- return walkMarkdownFiles2(commandsDir).sort((a, b) => a.localeCompare(b)).map((filePath) => {
7106
- const content = readFileSync6(filePath, "utf-8");
7107
- const { frontmatterLines, body } = splitMarkdownFrontmatter3(content);
7108
- const relativeStem = relative3(commandsDir, filePath).replace(/\\/g, "/").replace(/\.md$/i, "");
7109
- const commandId = relativeStem.replace(/\//g, "-").toLowerCase();
7110
- const title = firstHeading3(body) ?? commandId;
7111
- return {
7112
- filePath,
7113
- relativeStem,
7114
- commandId,
7115
- title,
7116
- description: parseCommandFrontmatterDescription(frontmatterLines),
7117
- argumentHint: parseCommandFrontmatterString(frontmatterLines, "argument-hint"),
7118
- agent: parseCommandFrontmatterString(frontmatterLines, "agent"),
7119
- subtask: parseCommandFrontmatterBoolean(frontmatterLines, "subtask"),
7120
- model: parseCommandFrontmatterString(frontmatterLines, "model"),
7121
- body: body.trim()
7122
- };
7123
- });
7124
- }
7125
- function getCanonicalCommandMetadata(command2) {
7126
- return {
7127
- commandId: command2.commandId,
7128
- title: command2.title,
7129
- ...command2.description ? { description: command2.description } : {},
7130
- ...command2.argumentHint ? { argumentHint: command2.argumentHint } : {},
7131
- ...command2.agent ? { agent: command2.agent } : {},
7132
- ...typeof command2.subtask === "boolean" ? { subtask: command2.subtask } : {},
7133
- ...command2.model ? { model: command2.model } : {},
7134
- template: command2.body
7135
- };
7720
+ if (metadata.context) {
7721
+ lines.push(` - Context hint: ${metadata.context}`);
7722
+ }
7723
+ return lines;
7136
7724
  }
7137
7725
 
7138
7726
  // src/generators/codex/index.ts
@@ -7148,7 +7736,10 @@ var CodexGenerator = class extends Generator {
7148
7736
  const entry = {
7149
7737
  url: server.url
7150
7738
  };
7151
- if (server.auth?.type === "bearer" && server.auth.envVar) {
7739
+ const nativeOverride = getNativeCodexMcpEntryOverride(this.config, name);
7740
+ if (nativeOverride) {
7741
+ Object.assign(entry, nativeOverride);
7742
+ } else if (server.auth?.type === "bearer" && server.auth.envVar) {
7152
7743
  entry.bearer_token_env_var = server.auth.envVar;
7153
7744
  } else if (server.auth?.type === "header" && server.auth.envVar) {
7154
7745
  const isBearerAuthorizationHeader = server.auth.headerName === "Authorization" && server.auth.headerTemplate === "Bearer ${value}";
@@ -7176,6 +7767,7 @@ var CodexGenerator = class extends Generator {
7176
7767
  this.generateHooksCompanion(),
7177
7768
  this.generateReadinessCompanion(),
7178
7769
  this.generateCommandsCompanion(),
7770
+ this.generateSkillsCompanion(),
7179
7771
  this.generatePermissionsCompanion()
7180
7772
  ]);
7181
7773
  this.copySkills();
@@ -7293,7 +7885,13 @@ var CodexGenerator = class extends Generator {
7293
7885
  lines.push(`sandbox_mode = ${JSON.stringify(metadata.sandboxMode)}`);
7294
7886
  }
7295
7887
  const delegationNotes = buildDelegationBehaviorNotes(agent.frontmatter);
7888
+ const fieldTranslationNote = getAgentTranslationMessage("codex", getTranslatedAgentFields("codex", metadata));
7296
7889
  const developerInstructions = [
7890
+ ...fieldTranslationNote ? [
7891
+ "Host translation note:",
7892
+ `- ${fieldTranslationNote}`,
7893
+ ""
7894
+ ] : [],
7297
7895
  ...delegationNotes.length > 0 ? [
7298
7896
  "Delegation contract:",
7299
7897
  ...delegationNotes.map((note) => `- ${note}`),
@@ -7334,7 +7932,18 @@ var CodexGenerator = class extends Generator {
7334
7932
  const codexEvent = mapHookEventToPascalCase(event);
7335
7933
  const mappedEntries = [];
7336
7934
  for (const entry of entries) {
7337
- if (entry.type === "prompt" || !entry.command) continue;
7935
+ const entryType = entry.type ?? "command";
7936
+ if (!isHookTypeSupported("codex", entryType)) {
7937
+ const issue = entryType === "command" ? null : getHookTypeTranslationIssue("codex", entryType);
7938
+ unsupported.push({
7939
+ canonicalEvent: event,
7940
+ codexEvent,
7941
+ type: entryType,
7942
+ reason: issue?.message ?? `Codex currently bundles only command-hook entries from Pluxx. ${entryType} hooks will be dropped from the generated Codex bundle.`
7943
+ });
7944
+ continue;
7945
+ }
7946
+ if (!entry.command) continue;
7338
7947
  nextWrapperIndex += 1;
7339
7948
  const relativePath = `hooks/pluxx-hook-command-${nextWrapperIndex}.sh`;
7340
7949
  await this.writeFile(
@@ -7407,22 +8016,62 @@ var CodexGenerator = class extends Generator {
7407
8016
  await this.writeJson(".codex/commands.generated.json", {
7408
8017
  model: "pluxx.commands.v1",
7409
8018
  nativeSurface: "degraded-to-guidance",
7410
- note: "Codex does not currently document plugin-packaged slash commands. Use these canonical command entries as workflow routing guidance alongside AGENTS.md.",
8019
+ note: `${getCodexCommandGuidanceNote()} Use these canonical command entries as workflow routing guidance alongside AGENTS.md.`,
7411
8020
  commands: commands.map((command2) => {
7412
8021
  const metadata = getCanonicalCommandMetadata(command2);
7413
8022
  return {
7414
8023
  id: metadata.commandId,
7415
8024
  title: metadata.title,
7416
8025
  ...metadata.description ? { description: metadata.description } : {},
8026
+ ...metadata.whenToUse ? { whenToUse: metadata.whenToUse } : {},
7417
8027
  ...metadata.argumentHint ? { argumentHint: metadata.argumentHint } : {},
8028
+ ...metadata.arguments.length > 0 ? { arguments: metadata.arguments } : {},
8029
+ ...metadata.examples.length > 0 ? { examples: metadata.examples } : {},
8030
+ ...metadata.skill ? { skill: metadata.skill } : {},
8031
+ ...metadata.skills.length > 0 ? { skills: metadata.skills } : {},
7418
8032
  ...metadata.agent ? { agent: metadata.agent } : {},
7419
8033
  ...typeof metadata.subtask === "boolean" ? { subtask: metadata.subtask } : {},
7420
8034
  ...metadata.model ? { model: metadata.model } : {},
8035
+ ...metadata.context ? { context: metadata.context } : {},
7421
8036
  template: metadata.template
7422
8037
  };
7423
8038
  })
7424
8039
  });
7425
8040
  }
8041
+ async generateSkillsCompanion() {
8042
+ if (!this.config.skills) return;
8043
+ const skillsDir = this.resolveConfigPath(this.config.skills, "skills");
8044
+ const skills = readCanonicalSkillFiles(skillsDir);
8045
+ if (skills.length === 0) return;
8046
+ await this.writeJson(".codex/skills.generated.json", {
8047
+ model: "pluxx.skills.v1",
8048
+ nativeSurface: "compatibility-skill-files-plus-guidance",
8049
+ note: "Codex preserves canonical SKILL.md files, but richer discovery and support-file metadata may also need command or AGENTS.md guidance. Use this companion to keep that richer canonical metadata visible after translation.",
8050
+ skills: skills.map((skill) => {
8051
+ const metadata = getCanonicalSkillMetadata(skill);
8052
+ return {
8053
+ id: metadata.dirName,
8054
+ title: metadata.title,
8055
+ ...metadata.description ? { description: metadata.description } : {},
8056
+ ...metadata.whenToUse ? { whenToUse: metadata.whenToUse } : {},
8057
+ ...metadata.argumentHint ? { argumentHint: metadata.argumentHint } : {},
8058
+ ...metadata.arguments.length > 0 ? { arguments: metadata.arguments } : {},
8059
+ ...typeof metadata.disableModelInvocation === "boolean" ? { disableModelInvocation: metadata.disableModelInvocation } : {},
8060
+ ...typeof metadata.userInvocable === "boolean" ? { userInvocable: metadata.userInvocable } : {},
8061
+ ...metadata.allowedTools.length > 0 ? { allowedTools: metadata.allowedTools } : {},
8062
+ ...metadata.model ? { model: metadata.model } : {},
8063
+ ...metadata.effort ? { effort: metadata.effort } : {},
8064
+ ...metadata.context ? { context: metadata.context } : {},
8065
+ ...metadata.agent ? { agent: metadata.agent } : {},
8066
+ ...metadata.paths.length > 0 ? { paths: metadata.paths } : {},
8067
+ ...metadata.shell ? { shell: metadata.shell } : {},
8068
+ ...metadata.helperScripts.length > 0 ? { helperScripts: metadata.helperScripts } : {},
8069
+ ...metadata.examplePaths.length > 0 ? { examplePaths: metadata.examplePaths } : {},
8070
+ ...metadata.referencePaths.length > 0 ? { referencePaths: metadata.referencePaths } : {}
8071
+ };
8072
+ })
8073
+ });
8074
+ }
7426
8075
  buildCodexCommandRoutingSection() {
7427
8076
  if (!this.config.commands) return null;
7428
8077
  const commandsDir = this.resolveConfigPath(this.config.commands, "commands");
@@ -7433,10 +8082,7 @@ var CodexGenerator = class extends Generator {
7433
8082
  "",
7434
8083
  "This plugin defines canonical command entrypoints. Codex does not package them as native slash commands today, so route those requests through the matching workflow directly.",
7435
8084
  "",
7436
- ...commands.map((command2) => {
7437
- const metadata = getCanonicalCommandMetadata(command2);
7438
- return `- \`/${metadata.commandId}\` - ${metadata.description ?? metadata.title}${metadata.argumentHint ? ` (arguments: ${metadata.argumentHint})` : ""}`;
7439
- })
8085
+ ...commands.flatMap((command2) => buildCodexCommandRoutingEntry(getCanonicalCommandMetadata(command2)))
7440
8086
  ];
7441
8087
  return lines.join("\n");
7442
8088
  }
@@ -7451,7 +8097,8 @@ var OpenCodeGenerator = class extends Generator {
7451
8097
  await Promise.all([
7452
8098
  this.generatePackageJson(),
7453
8099
  this.generatePluginWrapper(),
7454
- this.generateReadinessRuntime()
8100
+ this.generateReadinessRuntime(),
8101
+ this.generateSkillsCompanion()
7455
8102
  ]);
7456
8103
  this.copySkills();
7457
8104
  this.copyCommands();
@@ -7769,6 +8416,40 @@ var OpenCodeGenerator = class extends Generator {
7769
8416
  lines.push("");
7770
8417
  await this.writeFile("index.ts", lines.join("\n"));
7771
8418
  }
8419
+ async generateSkillsCompanion() {
8420
+ if (!this.config.skills) return;
8421
+ const skillsDir = this.resolveConfigPath(this.config.skills, "skills");
8422
+ const skills = readCanonicalSkillFiles(skillsDir);
8423
+ if (skills.length === 0) return;
8424
+ await this.writeJson("skills.generated.json", {
8425
+ model: "pluxx.skills.v1",
8426
+ nativeSurface: "skill-files-plus-runtime-guidance",
8427
+ note: "OpenCode preserves canonical skill files, but richer discovery, helper-script, and support-file metadata may still need command, agent, or runtime guidance. This companion keeps that richer canonical metadata visible after translation.",
8428
+ skills: skills.map((skill) => {
8429
+ const metadata = getCanonicalSkillMetadata(skill);
8430
+ return {
8431
+ id: metadata.dirName,
8432
+ title: metadata.title,
8433
+ ...metadata.description ? { description: metadata.description } : {},
8434
+ ...metadata.whenToUse ? { whenToUse: metadata.whenToUse } : {},
8435
+ ...metadata.argumentHint ? { argumentHint: metadata.argumentHint } : {},
8436
+ ...metadata.arguments.length > 0 ? { arguments: metadata.arguments } : {},
8437
+ ...typeof metadata.disableModelInvocation === "boolean" ? { disableModelInvocation: metadata.disableModelInvocation } : {},
8438
+ ...typeof metadata.userInvocable === "boolean" ? { userInvocable: metadata.userInvocable } : {},
8439
+ ...metadata.allowedTools.length > 0 ? { allowedTools: metadata.allowedTools } : {},
8440
+ ...metadata.model ? { model: metadata.model } : {},
8441
+ ...metadata.effort ? { effort: metadata.effort } : {},
8442
+ ...metadata.context ? { context: metadata.context } : {},
8443
+ ...metadata.agent ? { agent: metadata.agent } : {},
8444
+ ...metadata.paths.length > 0 ? { paths: metadata.paths } : {},
8445
+ ...metadata.shell ? { shell: metadata.shell } : {},
8446
+ ...metadata.helperScripts.length > 0 ? { helperScripts: metadata.helperScripts } : {},
8447
+ ...metadata.examplePaths.length > 0 ? { examplePaths: metadata.examplePaths } : {},
8448
+ ...metadata.referencePaths.length > 0 ? { referencePaths: metadata.referencePaths } : {}
8449
+ };
8450
+ })
8451
+ });
8452
+ }
7772
8453
  async generateReadinessRuntime() {
7773
8454
  if (!this.config.readiness) return;
7774
8455
  await this.writeFile("runtime/pluxx-readiness.mjs", buildGeneratedReadinessScript(this.config.readiness));
@@ -7810,10 +8491,16 @@ var OpenCodeGenerator = class extends Generator {
7810
8491
  output[metadata.commandId] = {
7811
8492
  template: metadata.template,
7812
8493
  ...metadata.description ? { description: metadata.description } : {},
8494
+ ...metadata.whenToUse ? { whenToUse: metadata.whenToUse } : {},
7813
8495
  ...metadata.argumentHint ? { argumentHint: metadata.argumentHint } : {},
8496
+ ...metadata.arguments.length > 0 ? { arguments: metadata.arguments } : {},
8497
+ ...metadata.examples.length > 0 ? { examples: metadata.examples } : {},
8498
+ ...metadata.skill ? { skill: metadata.skill } : {},
8499
+ ...metadata.skills.length > 0 ? { skills: metadata.skills } : {},
7814
8500
  ...metadata.agent ? { agent: metadata.agent } : {},
7815
8501
  ...typeof metadata.subtask === "boolean" ? { subtask: metadata.subtask } : {},
7816
- ...metadata.model ? { model: metadata.model } : {}
8502
+ ...metadata.model ? { model: metadata.model } : {},
8503
+ ...metadata.context ? { context: metadata.context } : {}
7817
8504
  };
7818
8505
  }
7819
8506
  return output;
@@ -9132,8 +9819,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
9132
9819
  },
9133
9820
  commands: {
9134
9821
  mode: "degrade",
9135
- nativeSurfaces: ["skills/", "AGENTS.md"],
9136
- notes: "Current Codex docs do not document plugin-packaged slash-command parity."
9822
+ nativeSurfaces: ["skills/", "AGENTS.md", ".codex/commands.generated.json"],
9823
+ notes: "Current Codex docs do not document plugin-packaged slash-command parity, so Pluxx keeps canonical command intent through routing guidance plus a generated companion mirror."
9137
9824
  },
9138
9825
  agents: {
9139
9826
  mode: "translate",
@@ -9293,7 +9980,7 @@ function renderPrimitiveTranslationSummary(summary) {
9293
9980
  }
9294
9981
 
9295
9982
  // src/branding-completeness.ts
9296
- function asRecord(value) {
9983
+ function asRecord2(value) {
9297
9984
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
9298
9985
  return value;
9299
9986
  }
@@ -9305,7 +9992,7 @@ function hasNonEmptyStringArray(value) {
9305
9992
  }
9306
9993
  function getBrandingCompletenessWarnings(config) {
9307
9994
  const warnings = [];
9308
- const codexInterface = asRecord(config.platforms?.codex?.interface);
9995
+ const codexInterface = asRecord2(config.platforms?.codex?.interface);
9309
9996
  if (config.targets.includes("codex")) {
9310
9997
  const codexHasIcon = hasNonEmptyString(config.brand?.icon) || hasNonEmptyString(codexInterface?.composerIcon) || hasNonEmptyString(codexInterface?.logo);
9311
9998
  const codexHasScreenshots = hasNonEmptyStringArray(config.brand?.screenshots) || hasNonEmptyStringArray(codexInterface?.screenshots);
@@ -9347,6 +10034,54 @@ function getBrandingCompletenessWarnings(config) {
9347
10034
  return warnings;
9348
10035
  }
9349
10036
 
10037
+ // src/skill-translation-registry.ts
10038
+ var CODEX_SKILL_FIELD_TRANSLATIONS = {
10039
+ when_to_use: "Pluxx currently keeps `when_to_use` as compatibility metadata in Codex skill files and may also surface the same discovery hint through command or routing guidance, because Codex does not document an equivalent skill discovery field.",
10040
+ "argument-hint": "Pluxx currently translates `argument-hint` into Codex command or routing guidance rather than documented Codex skill frontmatter.",
10041
+ arguments: "Pluxx currently translates skill `arguments` into Codex command or routing guidance rather than documented Codex skill frontmatter.",
10042
+ "user-invocable": "Pluxx currently keeps `user-invocable` as compatibility metadata in Codex skill files because Codex does not document an equivalent skill visibility field.",
10043
+ "allowed-tools": "Pluxx currently translates `allowed-tools` into Codex permission companions or external config rather than documented Codex skill frontmatter.",
10044
+ model: "Pluxx currently translates skill `model` intent through Codex custom agents, routing guidance, or model overrides rather than documented Codex skill frontmatter.",
10045
+ effort: "Pluxx currently translates skill `effort` intent through Codex custom-agent model reasoning controls or surrounding model guidance rather than documented Codex skill frontmatter.",
10046
+ context: "Pluxx currently treats skill `context` as companion instruction intent because Codex does not document an equivalent skill frontmatter field.",
10047
+ agent: "Pluxx currently translates skill `agent` intent through Codex custom agents or routing guidance rather than documented Codex skill frontmatter.",
10048
+ hooks: "Pluxx currently translates skill-local `hooks` intent through bundled Codex hooks, where skill-local attachment is lost.",
10049
+ paths: "Pluxx currently translates skill `paths` into surrounding instruction or routing context because Codex does not document an equivalent skill frontmatter field.",
10050
+ shell: "Pluxx currently translates skill `shell` intent through command or runtime guidance because Codex does not document an equivalent skill frontmatter field."
10051
+ };
10052
+ var OPENCODE_SKILL_FIELD_TRANSLATIONS = {
10053
+ when_to_use: "Pluxx currently keeps `when_to_use` as compatibility metadata in OpenCode skill files and may also surface the same discovery hint through commands or neighboring runtime guidance, because OpenCode does not document an equivalent skill discovery field.",
10054
+ "argument-hint": "Pluxx currently translates `argument-hint` into OpenCode commands or runtime command metadata rather than documented OpenCode skill frontmatter.",
10055
+ arguments: "Pluxx currently translates skill `arguments` into OpenCode commands or runtime command metadata rather than documented OpenCode skill frontmatter.",
10056
+ "user-invocable": "Pluxx currently keeps `user-invocable` as compatibility metadata in OpenCode skill files because OpenCode does not document an equivalent skill visibility field.",
10057
+ "allowed-tools": "Pluxx currently translates `allowed-tools` into OpenCode permission config rather than documented OpenCode skill frontmatter.",
10058
+ model: "Pluxx currently translates skill `model` intent through OpenCode agent/config model selection rather than documented OpenCode skill frontmatter.",
10059
+ effort: "Pluxx currently translates skill `effort` intent through OpenCode model configuration or agent/runtime controls rather than documented OpenCode skill frontmatter.",
10060
+ context: "Pluxx currently translates skill `context` through runtime instruction injection or neighboring config because OpenCode does not document an equivalent skill frontmatter field.",
10061
+ agent: "Pluxx currently translates skill `agent` intent through OpenCode agents or runtime routing rather than documented OpenCode skill frontmatter.",
10062
+ hooks: "Pluxx currently translates skill-local `hooks` intent through OpenCode runtime event handlers, where skill-local attachment is lost.",
10063
+ paths: "Pluxx currently translates skill `paths` into runtime or instruction context because OpenCode does not document an equivalent skill frontmatter field.",
10064
+ shell: "Pluxx currently translates skill `shell` intent through runtime code or command execution guidance because OpenCode does not document an equivalent skill frontmatter field."
10065
+ };
10066
+ function getSkillFrontmatterTranslationIssue(platform, key, supportedFields) {
10067
+ if (platform === "cursor") {
10068
+ return {
10069
+ code: "cursor-skill-frontmatter-unsupported",
10070
+ message: `Skill frontmatter field "${key}" is not supported by Cursor. Supported: ${supportedFields.join(", ")}`
10071
+ };
10072
+ }
10073
+ if (platform === "codex") {
10074
+ return {
10075
+ code: "codex-skill-frontmatter-translation",
10076
+ message: CODEX_SKILL_FIELD_TRANSLATIONS[key] ?? `Skill frontmatter field "${key}" is not part of documented Codex skill frontmatter. Pluxx may need to translate that intent through AGENTS.md, .codex/agents/*.toml, permissions companions, or runtime config instead of preserving it on SKILL.md.`
10077
+ };
10078
+ }
10079
+ return {
10080
+ code: "opencode-skill-frontmatter-translation",
10081
+ message: OPENCODE_SKILL_FIELD_TRANSLATIONS[key] ?? `Skill frontmatter field "${key}" is not part of documented OpenCode skill frontmatter. Pluxx may need to translate that intent through commands, agents, opencode.json, or plugin runtime code instead of preserving it on SKILL.md.`
10082
+ };
10083
+ }
10084
+
9350
10085
  // src/runtime-script-contract.ts
9351
10086
  var INSTALLER_OWNED_CHECK_ENV_PATH = "scripts/check-env.sh";
9352
10087
  var RUNTIME_SCRIPT_ROLE_PATHS = {
@@ -9493,7 +10228,7 @@ function isCodexManifestRelativePath(path) {
9493
10228
  if (relativePath.includes("..")) return false;
9494
10229
  return !relativePath.startsWith("/") && !relativePath.startsWith("\\");
9495
10230
  }
9496
- function asRecord2(value) {
10231
+ function asRecord3(value) {
9497
10232
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
9498
10233
  return value;
9499
10234
  }
@@ -9660,7 +10395,7 @@ function lintBrandMetadata(config, issues) {
9660
10395
  }
9661
10396
  function lintCodexOverrides(config, issues) {
9662
10397
  if (!isCodexTargetEnabled(config)) return;
9663
- const iface = asRecord2(config.platforms?.codex?.interface);
10398
+ const iface = asRecord3(config.platforms?.codex?.interface);
9664
10399
  if (!iface) return;
9665
10400
  if (typeof iface.brandColor === "string" && !HEX_COLOR_REGEX.test(iface.brandColor)) {
9666
10401
  pushIssue(issues, {
@@ -10202,45 +10937,44 @@ function lintAgentTranslationExplainability(dir, config, issues) {
10202
10937
  const metadata = getCanonicalAgentMetadata(agent);
10203
10938
  const relativePath = relative7(dir, agent.filePath).replace(/\\/g, "/");
10204
10939
  if (config.targets.includes("cursor")) {
10205
- const cursorDegradedFields = [];
10206
- if (metadata.hidden) cursorDegradedFields.push("hidden");
10207
- if (metadata.modelReasoningEffort) cursorDegradedFields.push("model_reasoning_effort");
10208
- if (metadata.sandboxMode) cursorDegradedFields.push("sandbox_mode");
10209
- if (metadata.permission) cursorDegradedFields.push("permission");
10210
- if (metadata.tools !== void 0) cursorDegradedFields.push("tools");
10211
- if (metadata.memory) cursorDegradedFields.push("memory");
10212
- if (typeof metadata.background === "boolean") cursorDegradedFields.push("background");
10213
- if (metadata.isolation) cursorDegradedFields.push("isolation");
10214
- if (cursorDegradedFields.length > 0) {
10940
+ const cursorDegradedFields = getTranslatedAgentFields("cursor", metadata);
10941
+ const message = getAgentTranslationMessage("cursor", cursorDegradedFields);
10942
+ if (message) {
10215
10943
  pushIssue(issues, {
10216
10944
  level: "warning",
10217
10945
  code: "cursor-agent-translation",
10218
- message: `Agent fields ${cursorDegradedFields.map((field) => `"${field}"`).join(", ")} are not preserved as first-class Cursor agent frontmatter today. Pluxx translates that intent through subagent framing and generated notes instead.`,
10946
+ message,
10219
10947
  file: relativePath,
10220
10948
  platform: "Cursor"
10221
10949
  });
10222
10950
  }
10223
10951
  }
10224
10952
  if (config.targets.includes("codex")) {
10225
- const codexDegradedFields = [];
10226
- if (metadata.hidden) codexDegradedFields.push("hidden");
10227
- if (metadata.permission) codexDegradedFields.push("permission");
10228
- if (metadata.tools !== void 0) codexDegradedFields.push("tools");
10229
- if (metadata.skills) codexDegradedFields.push("skills");
10230
- if (metadata.memory) codexDegradedFields.push("memory");
10231
- if (typeof metadata.background === "boolean") codexDegradedFields.push("background");
10232
- if (metadata.isolation) codexDegradedFields.push("isolation");
10233
- if (metadata.color) codexDegradedFields.push("color");
10234
- if (codexDegradedFields.length > 0) {
10953
+ const codexDegradedFields = getTranslatedAgentFields("codex", metadata);
10954
+ const message = getAgentTranslationMessage("codex", codexDegradedFields);
10955
+ if (message) {
10235
10956
  pushIssue(issues, {
10236
10957
  level: "warning",
10237
10958
  code: "codex-agent-translation",
10238
- message: `Agent fields ${codexDegradedFields.map((field) => `"${field}"`).join(", ")} are not native Codex TOML fields today. Pluxx keeps the specialist behavior, but translates that intent through developer instructions and companion surfaces instead.`,
10959
+ message,
10239
10960
  file: relativePath,
10240
10961
  platform: "Codex"
10241
10962
  });
10242
10963
  }
10243
10964
  }
10965
+ if (config.targets.includes("opencode")) {
10966
+ const opencodeDegradedFields = getTranslatedAgentFields("opencode", metadata);
10967
+ const message = getAgentTranslationMessage("opencode", opencodeDegradedFields);
10968
+ if (message) {
10969
+ pushIssue(issues, {
10970
+ level: "warning",
10971
+ code: "opencode-agent-translation",
10972
+ message,
10973
+ file: relativePath,
10974
+ platform: "OpenCode"
10975
+ });
10976
+ }
10977
+ }
10244
10978
  }
10245
10979
  }
10246
10980
  function lintCommandTranslationExplainability(dir, config, issues) {
@@ -10250,15 +10984,13 @@ function lintCommandTranslationExplainability(dir, config, issues) {
10250
10984
  const metadata = getCanonicalCommandMetadata(command2);
10251
10985
  const relativePath = relative7(dir, command2.filePath).replace(/\\/g, "/");
10252
10986
  if (config.targets.includes("codex")) {
10253
- const degradedFields = [];
10254
- if (metadata.agent) degradedFields.push("agent");
10255
- if (typeof metadata.subtask === "boolean") degradedFields.push("subtask");
10256
- if (metadata.model) degradedFields.push("model");
10257
- if (degradedFields.length > 0) {
10987
+ const degradedFields = getTranslatedCommandFields("codex", metadata);
10988
+ const message = getCommandTranslationMessage("codex", degradedFields);
10989
+ if (degradedFields.length > 0 && message) {
10258
10990
  pushIssue(issues, {
10259
10991
  level: "warning",
10260
10992
  code: "codex-command-translation",
10261
- message: `Command fields ${degradedFields.map((field) => `"${field}"`).join(", ")} are not native Codex plugin slash-command fields today. Pluxx keeps the workflow, but degrades those semantics into AGENTS.md routing guidance and \`.codex/commands.generated.json\`.`,
10993
+ message,
10262
10994
  file: relativePath,
10263
10995
  platform: "Codex"
10264
10996
  });
@@ -10340,9 +11072,9 @@ function lintLegacyCommandsDir(dir, config, issues) {
10340
11072
  }
10341
11073
  function lintCodexAgentsConfig(config, issues) {
10342
11074
  if (!isCodexTargetEnabled(config)) return;
10343
- const codexOverrides = asRecord2(config.platforms?.codex);
11075
+ const codexOverrides = asRecord3(config.platforms?.codex);
10344
11076
  if (!codexOverrides) return;
10345
- const agents = asRecord2(codexOverrides.agents);
11077
+ const agents = asRecord3(codexOverrides.agents);
10346
11078
  if (!agents) return;
10347
11079
  if (typeof agents.max_threads === "number" && agents.max_threads < CODEX_RULES.agents.maxThreadsMin) {
10348
11080
  pushIssue(issues, {
@@ -10366,8 +11098,8 @@ function lintCodexAgentsConfig(config, issues) {
10366
11098
  function lintCodexHooksExternalConfig(config, issues) {
10367
11099
  if (!isCodexTargetEnabled(config) || !config.hooks) return;
10368
11100
  if (Object.keys(config.hooks).length === 0) return;
10369
- const codexOverrides = asRecord2(config.platforms?.codex);
10370
- const features = codexOverrides ? asRecord2(codexOverrides.features) : null;
11101
+ const codexOverrides = asRecord3(config.platforms?.codex);
11102
+ const features = codexOverrides ? asRecord3(codexOverrides.features) : null;
10371
11103
  const hasPluxxCodexHooksFlag = features && features.codex_hooks === true;
10372
11104
  if (hasPluxxCodexHooksFlag) return;
10373
11105
  pushIssue(issues, {
@@ -10423,12 +11155,14 @@ function lintRuntimeReadiness(config, issues) {
10423
11155
  }
10424
11156
  function lintCursorHooks(config, issues) {
10425
11157
  if (!config.targets.includes("cursor") || !config.hooks) return;
11158
+ const supportedCursorEvents = getSupportedHookEvents("cursor");
11159
+ const unsupportedCursorEventReason = getUnsupportedHookEventReason("cursor");
10426
11160
  for (const [hookEvent, hookEntries] of Object.entries(config.hooks)) {
10427
- if (!CURSOR_SUPPORTED_HOOK_EVENTS.includes(hookEvent)) {
11161
+ if (!isHookEventSupported("cursor", hookEvent)) {
10428
11162
  pushIssue(issues, {
10429
11163
  level: "warning",
10430
11164
  code: "cursor-hook-event-unknown",
10431
- message: `Cursor does not support hook event "${hookEvent}". Supported: ${CURSOR_SUPPORTED_HOOK_EVENTS.join(", ")}`,
11165
+ message: unsupportedCursorEventReason ? `Cursor does not support hook event "${hookEvent}". ${unsupportedCursorEventReason}` : `Cursor does not support hook event "${hookEvent}". Supported: ${supportedCursorEvents.join(", ")}`,
10432
11166
  file: "pluxx.config.ts",
10433
11167
  platform: "Cursor"
10434
11168
  });
@@ -10437,11 +11171,12 @@ function lintCursorHooks(config, issues) {
10437
11171
  for (const entry of hookEntries) {
10438
11172
  if (!entry || typeof entry !== "object") continue;
10439
11173
  const rec = entry;
10440
- if (rec.type && rec.type !== "command" && rec.type !== "prompt") {
11174
+ if (typeof rec.type === "string" && !isHookTypeSupported("cursor", rec.type)) {
11175
+ const issue = getHookTypeTranslationIssue("cursor", rec.type);
10441
11176
  pushIssue(issues, {
10442
11177
  level: "warning",
10443
- code: "cursor-hook-type-unsupported",
10444
- message: `Cursor does not document hook type "${String(rec.type)}". Pluxx currently preserves only command and prompt hooks on the Cursor hook surface.`,
11178
+ code: issue?.code ?? "cursor-hook-type-unsupported",
11179
+ message: issue?.message ?? `Cursor does not document hook type "${String(rec.type)}". Pluxx currently preserves only command and prompt hooks on the Cursor hook surface.`,
10445
11180
  file: "pluxx.config.ts",
10446
11181
  platform: "Cursor"
10447
11182
  });
@@ -10473,50 +11208,16 @@ function lintCursorSkillFrontmatter(config, skillFiles, issues, frontmatterCache
10473
11208
  for (const [key] of parsed.frontmatterFields) {
10474
11209
  for (const [target, supported] of supportedByTarget.entries()) {
10475
11210
  if (supported.has(key)) continue;
10476
- let issue;
10477
- if (target === "cursor") {
10478
- issue = {
10479
- code: "cursor-skill-frontmatter-unsupported",
10480
- message: `Skill frontmatter field "${key}" is not supported by Cursor. Supported: ${[...supported].join(", ")}`,
10481
- platform: "Cursor"
10482
- };
10483
- } else if (target === "codex") {
10484
- const codexFieldTranslation = {
10485
- "argument-hint": "Pluxx currently translates `argument-hint` into Codex command or routing guidance rather than documented Codex skill frontmatter.",
10486
- arguments: "Pluxx currently translates skill `arguments` into Codex command or routing guidance rather than documented Codex skill frontmatter.",
10487
- "allowed-tools": "Pluxx currently translates `allowed-tools` into Codex permission companions or external config rather than documented Codex skill frontmatter.",
10488
- context: "Pluxx currently treats skill `context` as companion instruction intent because Codex does not document an equivalent skill frontmatter field.",
10489
- agent: "Pluxx currently translates skill `agent` intent through Codex custom agents or routing guidance rather than documented Codex skill frontmatter.",
10490
- hooks: "Pluxx currently translates skill-local `hooks` intent through bundled Codex hooks, where skill-local attachment is lost.",
10491
- paths: "Pluxx currently translates skill `paths` into surrounding instruction or routing context because Codex does not document an equivalent skill frontmatter field.",
10492
- shell: "Pluxx currently translates skill `shell` intent through command or runtime guidance because Codex does not document an equivalent skill frontmatter field."
10493
- };
10494
- issue = {
10495
- code: "codex-skill-frontmatter-translation",
10496
- message: codexFieldTranslation[key] ?? `Skill frontmatter field "${key}" is not part of documented Codex skill frontmatter. Pluxx may need to translate that intent through AGENTS.md, .codex/agents/*.toml, permissions companions, or runtime config instead of preserving it on SKILL.md.`,
10497
- platform: "Codex"
10498
- };
10499
- } else {
10500
- const opencodeFieldTranslation = {
10501
- "argument-hint": "Pluxx currently translates `argument-hint` into OpenCode commands or runtime command metadata rather than documented OpenCode skill frontmatter.",
10502
- arguments: "Pluxx currently translates skill `arguments` into OpenCode commands or runtime command metadata rather than documented OpenCode skill frontmatter.",
10503
- "allowed-tools": "Pluxx currently translates `allowed-tools` into OpenCode permission config rather than documented OpenCode skill frontmatter.",
10504
- context: "Pluxx currently translates skill `context` through runtime instruction injection or neighboring config because OpenCode does not document an equivalent skill frontmatter field.",
10505
- agent: "Pluxx currently translates skill `agent` intent through OpenCode agents or runtime routing rather than documented OpenCode skill frontmatter.",
10506
- hooks: "Pluxx currently translates skill-local `hooks` intent through OpenCode runtime event handlers, where skill-local attachment is lost.",
10507
- paths: "Pluxx currently translates skill `paths` into runtime or instruction context because OpenCode does not document an equivalent skill frontmatter field.",
10508
- shell: "Pluxx currently translates skill `shell` intent through runtime code or command execution guidance because OpenCode does not document an equivalent skill frontmatter field."
10509
- };
10510
- issue = {
10511
- code: "opencode-skill-frontmatter-translation",
10512
- message: opencodeFieldTranslation[key] ?? `Skill frontmatter field "${key}" is not part of documented OpenCode skill frontmatter. Pluxx may need to translate that intent through commands, agents, opencode.json, or plugin runtime code instead of preserving it on SKILL.md.`,
10513
- platform: "OpenCode"
10514
- };
10515
- }
11211
+ const issue = getSkillFrontmatterTranslationIssue(
11212
+ target,
11213
+ key,
11214
+ [...supported]
11215
+ );
10516
11216
  pushIssue(issues, {
10517
11217
  level: "warning",
10518
11218
  file: skillFile,
10519
- ...issue
11219
+ ...issue,
11220
+ platform: target === "cursor" ? "Cursor" : target === "codex" ? "Codex" : "OpenCode"
10520
11221
  });
10521
11222
  }
10522
11223
  }
@@ -10535,69 +11236,111 @@ function lintHookFieldTranslations(config, issues) {
10535
11236
  );
10536
11237
  if (hasPromptHooks) {
10537
11238
  if (config.targets.includes("claude-code")) {
10538
- pushIssue(issues, {
10539
- level: "warning",
10540
- code: "claude-prompt-hook-degrade",
10541
- message: "Prompt hooks are documented in Claude-native hook surfaces, but the current Claude-family generator still drops prompt hooks. Expect a degraded result unless you remodel them as command hooks or host-specific manual work.",
10542
- file: "pluxx.config.ts",
10543
- platform: "claude-code"
10544
- });
11239
+ const warnedEvents = /* @__PURE__ */ new Set();
11240
+ for (const [event, entries] of Object.entries(config.hooks)) {
11241
+ if (!(entries ?? []).some((entry) => entry.type === "prompt")) continue;
11242
+ const mappedEvent = mapHookEventToPascalCase(event);
11243
+ const issue = getPromptHookTranslationIssue("claude-code", mappedEvent);
11244
+ if (!issue || warnedEvents.has(mappedEvent)) continue;
11245
+ warnedEvents.add(mappedEvent);
11246
+ pushIssue(issues, {
11247
+ level: "warning",
11248
+ file: "pluxx.config.ts",
11249
+ platform: "claude-code",
11250
+ ...issue
11251
+ });
11252
+ }
10545
11253
  }
10546
11254
  if (config.targets.includes("codex")) {
10547
- pushIssue(issues, {
10548
- level: "warning",
10549
- code: "codex-prompt-hook-drop",
10550
- message: "Codex currently receives only command-hook companions from Pluxx. Prompt hooks will be dropped from the generated Codex bundle.",
10551
- file: "pluxx.config.ts",
10552
- platform: "codex"
10553
- });
11255
+ const issue = getPromptHookTranslationIssue("codex");
11256
+ if (issue) {
11257
+ pushIssue(issues, {
11258
+ level: "warning",
11259
+ file: "pluxx.config.ts",
11260
+ platform: "codex",
11261
+ ...issue
11262
+ });
11263
+ }
10554
11264
  }
10555
11265
  if (config.targets.includes("opencode")) {
11266
+ const issue = getPromptHookTranslationIssue("opencode");
11267
+ if (issue) {
11268
+ pushIssue(issues, {
11269
+ level: "warning",
11270
+ file: "pluxx.config.ts",
11271
+ platform: "opencode",
11272
+ ...issue
11273
+ });
11274
+ }
11275
+ }
11276
+ }
11277
+ if (hasFailClosed && config.targets.includes("claude-code")) {
11278
+ const issue = getHookFieldTranslationIssue("claude-code", "failClosed");
11279
+ if (issue) {
10556
11280
  pushIssue(issues, {
10557
11281
  level: "warning",
10558
- code: "opencode-prompt-hook-drop",
10559
- message: "The current OpenCode runtime wrapper only emits command hooks. Prompt hooks will be dropped from the generated OpenCode plugin.",
10560
11282
  file: "pluxx.config.ts",
10561
- platform: "opencode"
11283
+ platform: "claude-code",
11284
+ ...issue
10562
11285
  });
10563
11286
  }
10564
11287
  }
10565
- if (hasFailClosed && config.targets.includes("claude-code")) {
10566
- pushIssue(issues, {
10567
- level: "warning",
10568
- code: "claude-hook-failclosed-degrade",
10569
- message: "Claude hook entries currently drop `failClosed` in generated output. Keep this behavior host-specific or verify the generated hook bundle carefully.",
10570
- file: "pluxx.config.ts",
10571
- platform: "claude-code"
10572
- });
10573
- }
10574
11288
  if (hasLoopLimit) {
10575
11289
  if (config.targets.includes("claude-code")) {
10576
- pushIssue(issues, {
10577
- level: "warning",
10578
- code: "claude-hook-loop-limit-degrade",
10579
- message: "Claude outputs currently drop `loop_limit`. Recursive hook protection is not preserved there today.",
10580
- file: "pluxx.config.ts",
10581
- platform: "claude-code"
10582
- });
11290
+ const issue = getHookFieldTranslationIssue("claude-code", "loop_limit");
11291
+ if (issue) {
11292
+ pushIssue(issues, {
11293
+ level: "warning",
11294
+ file: "pluxx.config.ts",
11295
+ platform: "claude-code",
11296
+ ...issue
11297
+ });
11298
+ }
10583
11299
  }
10584
11300
  if (config.targets.includes("codex")) {
10585
- pushIssue(issues, {
10586
- level: "warning",
10587
- code: "codex-hook-loop-limit-drop",
10588
- message: "Codex hook companions currently drop `loop_limit`. Only command, matcher, timeout, and failClosed survive there today.",
10589
- file: "pluxx.config.ts",
10590
- platform: "codex"
10591
- });
11301
+ const issue = getHookFieldTranslationIssue("codex", "loop_limit");
11302
+ if (issue) {
11303
+ pushIssue(issues, {
11304
+ level: "warning",
11305
+ file: "pluxx.config.ts",
11306
+ platform: "codex",
11307
+ ...issue
11308
+ });
11309
+ }
10592
11310
  }
10593
11311
  if (config.targets.includes("opencode")) {
10594
- pushIssue(issues, {
10595
- level: "warning",
10596
- code: "opencode-hook-loop-limit-drop",
10597
- message: "OpenCode runtime hooks currently drop `loop_limit`. Recursive hook protection is still Cursor-first in Pluxx.",
10598
- file: "pluxx.config.ts",
10599
- platform: "opencode"
10600
- });
11312
+ const issue = getHookFieldTranslationIssue("opencode", "loop_limit");
11313
+ if (issue) {
11314
+ pushIssue(issues, {
11315
+ level: "warning",
11316
+ file: "pluxx.config.ts",
11317
+ platform: "opencode",
11318
+ ...issue
11319
+ });
11320
+ }
11321
+ }
11322
+ }
11323
+ }
11324
+ function lintHookTypeTranslations(config, issues) {
11325
+ if (!config.hooks) return;
11326
+ const seen = /* @__PURE__ */ new Set();
11327
+ for (const entries of Object.values(config.hooks)) {
11328
+ for (const entry of entries ?? []) {
11329
+ if (!entry.type || entry.type === "command") continue;
11330
+ for (const target of ["codex", "opencode"]) {
11331
+ if (!config.targets.includes(target) || isHookTypeSupported(target, entry.type)) continue;
11332
+ const key = `${target}:${entry.type}`;
11333
+ if (seen.has(key)) continue;
11334
+ seen.add(key);
11335
+ const issue = getHookTypeTranslationIssue(target, entry.type);
11336
+ if (!issue) continue;
11337
+ pushIssue(issues, {
11338
+ level: "warning",
11339
+ file: "pluxx.config.ts",
11340
+ platform: target,
11341
+ ...issue
11342
+ });
11343
+ }
10601
11344
  }
10602
11345
  }
10603
11346
  }
@@ -10606,7 +11349,7 @@ function lintCodexCommandGuidance(config, issues) {
10606
11349
  pushIssue(issues, {
10607
11350
  level: "warning",
10608
11351
  code: "codex-commands-routing-guidance",
10609
- message: "Codex does not currently document plugin-packaged slash-command parity. Pluxx will degrade commands into skills plus AGENTS.md and `.codex/commands.generated.json` routing guidance.",
11352
+ message: `${getCodexCommandGuidanceNote()} Pluxx will degrade commands into skills plus those routing surfaces today.`,
10610
11353
  file: "pluxx.config.ts",
10611
11354
  platform: "codex"
10612
11355
  });
@@ -10816,6 +11559,7 @@ async function lintProject(dir = process.cwd(), options = {}) {
10816
11559
  lintPermissions(lintConfig, issues);
10817
11560
  lintPrimitiveTranslations(lintConfig, issues);
10818
11561
  lintHookFieldTranslations(lintConfig, issues);
11562
+ lintHookTypeTranslations(lintConfig, issues);
10819
11563
  lintCodexCommandGuidance(lintConfig, issues);
10820
11564
  lintCursorHooks(lintConfig, issues);
10821
11565
  lintCursorRuleContentLimits(lintConfig, issues);
@@ -10902,131 +11646,6 @@ import { resolve as resolve14 } from "path";
10902
11646
  import { existsSync as existsSync17, lstatSync as lstatSync2 } from "fs";
10903
11647
  import { mkdir as mkdir2 } from "fs/promises";
10904
11648
  import { basename as basename5, dirname as dirname4, relative as relative8, resolve as resolve13 } from "path";
10905
-
10906
- // src/user-config.ts
10907
- var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
10908
- var PLACEHOLDER_SECRET_PATTERNS = [
10909
- /\bdummy\b/i,
10910
- /\bplaceholder\b/i,
10911
- /\bexample\b/i,
10912
- /\bchangeme\b/i,
10913
- /\breplace[_ -]?me\b/i,
10914
- /\byour[_ -]?(api[_ -]?)?key\b/i,
10915
- /\bapi[_ -]?key[_ -]?here\b/i,
10916
- /\btoken[_ -]?here\b/i
10917
- ];
10918
- function normalizeUserConfigKey(value) {
10919
- return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[._/\s]+/g, "-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
10920
- }
10921
- function humanizeUserConfigLabel(value) {
10922
- return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[._/]+/g, " ").replace(/-/g, " ").split(/\s+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
10923
- }
10924
- function defaultUserConfigEnvVar(key) {
10925
- return key.replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
10926
- }
10927
- function extractEnvReference(value) {
10928
- if (!value) return void 0;
10929
- const match = value.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
10930
- return match?.[1];
10931
- }
10932
- function isPlaceholderSecretValue(value) {
10933
- if (typeof value !== "string") return false;
10934
- const normalized = value.trim();
10935
- if (normalized === "") return false;
10936
- return PLACEHOLDER_SECRET_PATTERNS.some((pattern) => pattern.test(normalized));
10937
- }
10938
- function isRuntimePlatformManaged(config, target, server) {
10939
- if (server.transport === "stdio") return false;
10940
- if (server.auth?.type === "platform") {
10941
- return true;
10942
- }
10943
- if (target === "claude-code") {
10944
- return config.platforms?.["claude-code"]?.mcpAuth === "platform";
10945
- }
10946
- if (target === "cursor") {
10947
- return config.platforms?.cursor?.mcpAuth === "platform";
10948
- }
10949
- return false;
10950
- }
10951
- function targetApplies(entry, target) {
10952
- return !entry.targets || entry.targets.includes(target);
10953
- }
10954
- function dedupeUserConfigEntries(entries) {
10955
- const deduped = [];
10956
- const seenKeys = /* @__PURE__ */ new Set();
10957
- const seenEnvVars = /* @__PURE__ */ new Set();
10958
- for (const entry of entries) {
10959
- const envVar = entry.envVar?.trim();
10960
- if (seenKeys.has(entry.key)) continue;
10961
- if (envVar && seenEnvVars.has(envVar)) continue;
10962
- deduped.push(entry);
10963
- seenKeys.add(entry.key);
10964
- if (envVar) seenEnvVars.add(envVar);
10965
- }
10966
- return deduped;
10967
- }
10968
- function collectUserConfigEntries(config, platforms = config.targets) {
10969
- const explicitEntries = (config.userConfig ?? []).filter((entry) => !entry.targets || entry.targets.some((target) => platforms.includes(target))).map((entry) => ({ ...entry, source: "explicit" }));
10970
- const derivedEntries = [];
10971
- for (const [serverName, server] of Object.entries(config.mcp ?? {})) {
10972
- const applicableTargets = platforms.filter((target) => !isRuntimePlatformManaged(config, target, server));
10973
- if (server.auth?.type && server.auth.type !== "none" && server.auth.type !== "platform" && ENV_VAR_NAME.test(server.auth.envVar) && applicableTargets.length > 0) {
10974
- derivedEntries.push({
10975
- key: normalizeUserConfigKey(server.auth.envVar),
10976
- title: humanizeUserConfigLabel(server.auth.envVar),
10977
- description: `Authentication credential for the ${serverName} MCP server.`,
10978
- type: "secret",
10979
- required: true,
10980
- envVar: server.auth.envVar,
10981
- targets: applicableTargets,
10982
- source: "mcp-auth"
10983
- });
10984
- }
10985
- if (server.transport === "stdio") {
10986
- for (const [key, rawValue] of Object.entries(server.env ?? {})) {
10987
- const envVar = extractEnvReference(rawValue) ?? (ENV_VAR_NAME.test(key) ? key : void 0);
10988
- if (!envVar || !ENV_VAR_NAME.test(envVar)) continue;
10989
- if (extractEnvReference(rawValue) === void 0 && rawValue !== "") {
10990
- continue;
10991
- }
10992
- derivedEntries.push({
10993
- key: normalizeUserConfigKey(envVar),
10994
- title: humanizeUserConfigLabel(envVar),
10995
- description: `Environment value required to launch the ${serverName} stdio MCP server.`,
10996
- type: "secret",
10997
- required: true,
10998
- envVar,
10999
- targets: applicableTargets.length > 0 ? applicableTargets : platforms,
11000
- source: "mcp-env"
11001
- });
11002
- }
11003
- }
11004
- }
11005
- return dedupeUserConfigEntries([
11006
- ...explicitEntries,
11007
- ...derivedEntries
11008
- ]);
11009
- }
11010
- function resolveUserConfigEntriesForTarget(entries, target) {
11011
- return entries.filter(({ field }) => targetApplies(field, target));
11012
- }
11013
- function buildUserConfigEnvMap(entries) {
11014
- const env = {};
11015
- for (const entry of entries) {
11016
- if (!entry.envVar) continue;
11017
- env[entry.envVar] = String(entry.value);
11018
- }
11019
- return env;
11020
- }
11021
- function buildUserConfigValueMap(entries) {
11022
- const values = {};
11023
- for (const entry of entries) {
11024
- values[entry.field.key] = entry.value;
11025
- }
11026
- return values;
11027
- }
11028
-
11029
- // src/cli/init-from-mcp.ts
11030
11649
  var MCP_SKILL_GROUPINGS = ["workflow", "tool"];
11031
11650
  var MCP_HOOK_MODES = ["none", "safe"];
11032
11651
  var MCP_RUNTIME_AUTH_MODES = ["inline", "platform"];
@@ -11243,17 +11862,29 @@ async function planMcpScaffold(options) {
11243
11862
  const instructionsPath = resolve13(options.rootDir, "INSTRUCTIONS.md");
11244
11863
  const skillRoot = resolve13(options.rootDir, "skills");
11245
11864
  const commandsRoot = resolve13(options.rootDir, "commands");
11865
+ const basePlatforms = runtimeAuthMode === "platform" ? {
11866
+ "claude-code": { mcpAuth: "platform" },
11867
+ cursor: { mcpAuth: "platform" }
11868
+ } : void 0;
11869
+ const platforms = mergePlatformConfigMaps(basePlatforms, options.platformOverrides);
11246
11870
  const userConfigSource = {
11247
11871
  targets: options.targets,
11248
11872
  mcp: {
11249
11873
  [serverName]: options.source
11250
11874
  },
11251
- platforms: runtimeAuthMode === "platform" ? {
11252
- "claude-code": { mcpAuth: "platform" },
11253
- cursor: { mcpAuth: "platform" }
11254
- } : void 0
11875
+ platforms
11255
11876
  };
11256
- const userConfig = collectUserConfigEntries(userConfigSource).map(({ source: _source, ...entry }) => entry);
11877
+ const baseUserConfig = collectUserConfigEntries(userConfigSource);
11878
+ const userConfig = [
11879
+ ...baseUserConfig,
11880
+ ...collectNativeMcpAuthUserConfigEntries(userConfigSource, options.targets, baseUserConfig)
11881
+ ].map((entry) => {
11882
+ if ("source" in entry) {
11883
+ const { source: _source, ...rest } = entry;
11884
+ return rest;
11885
+ }
11886
+ return entry;
11887
+ });
11257
11888
  const skillDirectories = [];
11258
11889
  const commandFiles = [];
11259
11890
  const generatedFiles = ["pluxx.config.ts", "./INSTRUCTIONS.md"];
@@ -11283,6 +11914,7 @@ async function planMcpScaffold(options) {
11283
11914
  passthroughPaths,
11284
11915
  runtimeAuthMode,
11285
11916
  permissions,
11917
+ platforms,
11286
11918
  commandsPath: plannedCommands.length > 0 ? "./commands/" : void 0
11287
11919
  })
11288
11920
  );
@@ -11521,6 +12153,7 @@ function buildSkillContent(skill) {
11521
12153
  function buildCommandContent(skill, existingContent) {
11522
12154
  const description = truncate(cleanSingleLineText(skill.description), 140);
11523
12155
  const argumentHint = inferCommandArgumentHint(skill);
12156
+ const argumentNames = inferCommandArgumentNames(skill);
11524
12157
  const entryBlurb = buildCommandEntryBlurb(skill);
11525
12158
  const hasRelatedResources = skill.resources.length > 0 || skill.resourceTemplates.length > 0;
11526
12159
  const hasRelatedPrompts = skill.prompts.length > 0;
@@ -11539,7 +12172,10 @@ function buildCommandContent(skill, existingContent) {
11539
12172
  const generatedContent = [
11540
12173
  "---",
11541
12174
  `description: ${JSON.stringify(description)}`,
12175
+ `when_to_use: ${JSON.stringify(entryBlurb)}`,
11542
12176
  `argument-hint: ${formatArgumentHintFrontmatter(argumentHint)}`,
12177
+ `arguments: ${JSON.stringify(argumentNames)}`,
12178
+ `skill: ${JSON.stringify(skill.dirName)}`,
11543
12179
  "---",
11544
12180
  "",
11545
12181
  entryBlurb,
@@ -11704,15 +12340,8 @@ function buildConfigTemplate(input) {
11704
12340
  const permissionsBlock = input.permissions ? `
11705
12341
  permissions: ${serializePermissions(input.permissions)},
11706
12342
  ` : "";
11707
- const platformsBlock = input.runtimeAuthMode === "platform" ? `
11708
- platforms: {
11709
- 'claude-code': {
11710
- mcpAuth: 'platform',
11711
- },
11712
- cursor: {
11713
- mcpAuth: 'platform',
11714
- },
11715
- },
12343
+ const platformsBlock = input.platforms ? `
12344
+ platforms: ${serializePlatformOverrides(input.platforms)},
11716
12345
  ` : "";
11717
12346
  return `import { definePlugin } from 'pluxx'
11718
12347
 
@@ -11861,6 +12490,88 @@ function serializePermissions(permissions) {
11861
12490
  ${entries}
11862
12491
  }`;
11863
12492
  }
12493
+ function mergePlatformConfigMaps(base, extra) {
12494
+ const merged = {
12495
+ ...base ?? {},
12496
+ ...extra ?? {}
12497
+ };
12498
+ if (base?.["claude-code"] || extra?.["claude-code"]) {
12499
+ merged["claude-code"] = {
12500
+ ...base?.["claude-code"] ?? {},
12501
+ ...extra?.["claude-code"] ?? {},
12502
+ ...base?.["claude-code"]?.mcpServers || extra?.["claude-code"]?.mcpServers ? {
12503
+ mcpServers: {
12504
+ ...base?.["claude-code"]?.mcpServers ?? {},
12505
+ ...extra?.["claude-code"]?.mcpServers ?? {}
12506
+ }
12507
+ } : {}
12508
+ };
12509
+ }
12510
+ if (base?.cursor || extra?.cursor) {
12511
+ merged.cursor = {
12512
+ ...base?.cursor ?? {},
12513
+ ...extra?.cursor ?? {},
12514
+ ...base?.cursor?.mcpServers || extra?.cursor?.mcpServers ? {
12515
+ mcpServers: {
12516
+ ...base?.cursor?.mcpServers ?? {},
12517
+ ...extra?.cursor?.mcpServers ?? {}
12518
+ }
12519
+ } : {}
12520
+ };
12521
+ }
12522
+ if (base?.codex || extra?.codex) {
12523
+ merged.codex = {
12524
+ ...base?.codex ?? {},
12525
+ ...extra?.codex ?? {},
12526
+ ...base?.codex?.mcpServers || extra?.codex?.mcpServers ? {
12527
+ mcpServers: {
12528
+ ...base?.codex?.mcpServers ?? {},
12529
+ ...extra?.codex?.mcpServers ?? {}
12530
+ }
12531
+ } : {}
12532
+ };
12533
+ }
12534
+ if (base?.opencode || extra?.opencode) {
12535
+ merged.opencode = {
12536
+ ...base?.opencode ?? {},
12537
+ ...extra?.opencode ?? {},
12538
+ ...base?.opencode?.mcpServers || extra?.opencode?.mcpServers ? {
12539
+ mcpServers: {
12540
+ ...base?.opencode?.mcpServers ?? {},
12541
+ ...extra?.opencode?.mcpServers ?? {}
12542
+ }
12543
+ } : {}
12544
+ };
12545
+ }
12546
+ return Object.keys(merged).length > 0 ? merged : void 0;
12547
+ }
12548
+ function serializePlatformOverrides(platforms) {
12549
+ return serializeTsLiteral(platforms, 1);
12550
+ }
12551
+ function serializeTsLiteral(value, indentLevel) {
12552
+ const indent = " ".repeat(indentLevel);
12553
+ const nextIndent = " ".repeat(indentLevel + 1);
12554
+ if (Array.isArray(value)) {
12555
+ if (value.length === 0) return "[]";
12556
+ return `[
12557
+ ${value.map((item) => `${nextIndent}${serializeTsLiteral(item, indentLevel + 1)}`).join(",\n")}
12558
+ ${indent}]`;
12559
+ }
12560
+ if (value && typeof value === "object") {
12561
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0);
12562
+ if (entries.length === 0) return "{}";
12563
+ return `{
12564
+ ${entries.map(([key, entryValue]) => `${nextIndent}${serializeTsKey(key)}: ${serializeTsLiteral(entryValue, indentLevel + 1)}`).join(",\n")}
12565
+ ${indent}}`;
12566
+ }
12567
+ if (typeof value === "string") {
12568
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
12569
+ }
12570
+ return JSON.stringify(value);
12571
+ }
12572
+ function serializeTsKey(key) {
12573
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${key}'`;
12574
+ }
11864
12575
  var MUTATING_PREFIXES = [
11865
12576
  "create",
11866
12577
  "add",
@@ -12374,6 +13085,13 @@ function buildInstructionSkillSummary(skill) {
12374
13085
  return `\`${skill.dirName}\`: ${description} Primary tools: ${toolNames}.${resourceNote}${promptNote}`;
12375
13086
  }
12376
13087
  function inferCommandArgumentHint(skill) {
13088
+ const fieldHints = inferCommandArgumentNames(skill);
13089
+ if (fieldHints.length === 0) {
13090
+ return "[request]";
13091
+ }
13092
+ return fieldHints.slice(0, 2).map((hint) => `[${hint}]`).join(" ");
13093
+ }
13094
+ function inferCommandArgumentNames(skill) {
12377
13095
  const fieldHints = /* @__PURE__ */ new Set();
12378
13096
  for (const tool of skill.tools) {
12379
13097
  const fields = getTopLevelSchemaFields(tool.inputSchema);
@@ -12386,9 +13104,9 @@ function inferCommandArgumentHint(skill) {
12386
13104
  if (fieldHints.size >= 2) break;
12387
13105
  }
12388
13106
  if (fieldHints.size === 0) {
12389
- return "[request]";
13107
+ return ["request"];
12390
13108
  }
12391
- return [...fieldHints].slice(0, 2).map((hint) => `[${hint}]`).join(" ");
13109
+ return [...fieldHints].slice(0, 2);
12392
13110
  }
12393
13111
  function buildCommandEntryBlurb(skill) {
12394
13112
  const intent = inferSkillIntentPhrase(skill);
@@ -14798,12 +15516,21 @@ function loadManualAgentProjectModel(rootDir, config) {
14798
15516
  const skillsDir = config.skills ? resolve17(rootDir, config.skills) : void 0;
14799
15517
  const commandsDir = config.commands ? resolve17(rootDir, config.commands) : void 0;
14800
15518
  const skills = readCanonicalSkillFiles2(rootDir, skillsDir);
14801
- const commands = readCanonicalCommandFiles(commandsDir).map((command2) => ({
14802
- path: normalizeRelativePath(relative10(rootDir, command2.filePath)),
14803
- title: command2.title,
14804
- description: command2.description,
14805
- argumentHint: command2.argumentHint
14806
- }));
15519
+ const commands = readCanonicalCommandFiles(commandsDir).map((command2) => {
15520
+ const metadata = getCanonicalCommandMetadata(command2);
15521
+ return {
15522
+ path: normalizeRelativePath(relative10(rootDir, command2.filePath)),
15523
+ title: metadata.title,
15524
+ description: metadata.description,
15525
+ whenToUse: metadata.whenToUse,
15526
+ argumentHint: metadata.argumentHint,
15527
+ ...metadata.arguments.length > 0 ? { arguments: metadata.arguments } : {},
15528
+ ...metadata.examples.length > 0 ? { examples: metadata.examples } : {},
15529
+ ...metadata.skill ? { skill: metadata.skill } : {},
15530
+ ...metadata.skills.length > 0 ? { skills: metadata.skills } : {},
15531
+ ...metadata.agent ? { agent: metadata.agent } : {}
15532
+ };
15533
+ });
14807
15534
  return {
14808
15535
  sourceKind: "manual",
14809
15536
  displayName,
@@ -14872,6 +15599,9 @@ function buildAgentContext(config, project, lint, contextSources, ingestion, doc
14872
15599
  if (skill.description) {
14873
15600
  lines.push(`- Description: ${skill.description}`);
14874
15601
  }
15602
+ if ((skill.supportPaths?.length ?? 0) > 0) {
15603
+ lines.push(`- Related files: ${skill.supportPaths?.map((path) => `\`${path}\``).join(", ")}`);
15604
+ }
14875
15605
  if (relatedResourceLabels.length > 0) {
14876
15606
  lines.push(`- Related resources: ${relatedResourceLabels.join(", ")}`);
14877
15607
  }
@@ -14884,7 +15614,18 @@ function buildAgentContext(config, project, lint, contextSources, ingestion, doc
14884
15614
  lines.push("## Commands");
14885
15615
  lines.push("");
14886
15616
  for (const command2 of project.commands) {
14887
- lines.push(`- \`${command2.path}\`: ${command2.description ?? command2.title}${command2.argumentHint ? ` (arguments: ${command2.argumentHint})` : ""}`);
15617
+ const details = [
15618
+ command2.argumentHint ? `arguments: ${command2.argumentHint}` : null,
15619
+ command2.skill ? `skill: ${command2.skill}` : null,
15620
+ command2.agent ? `agent: ${command2.agent}` : null
15621
+ ].filter(Boolean);
15622
+ lines.push(`- \`${command2.path}\`: ${command2.description ?? command2.title}${details.length > 0 ? ` (${details.join("; ")})` : ""}`);
15623
+ if (command2.whenToUse) {
15624
+ lines.push(` - When to use: ${command2.whenToUse}`);
15625
+ }
15626
+ if ((command2.examples?.length ?? 0) > 0) {
15627
+ lines.push(` - Examples: ${command2.examples?.map((example) => `\`${example}\``).join(", ")}`);
15628
+ }
14888
15629
  }
14889
15630
  lines.push("");
14890
15631
  }
@@ -16301,13 +17042,17 @@ function normalizeRelativePath(path) {
16301
17042
  return path.replace(/\\/g, "/").replace(/^\.\//, "");
16302
17043
  }
16303
17044
  function readCanonicalSkillFiles2(rootDir, skillsDir) {
16304
- return readCanonicalSkillFiles(skillsDir).map((skill) => ({
16305
- dirName: normalizeRelativePath(skill.dirName),
16306
- title: skill.firstHeading ?? skill.name ?? skill.dirName,
16307
- description: skill.description,
16308
- toolNames: [],
16309
- path: normalizeRelativePath(relative10(rootDir, skill.filePath))
16310
- }));
17045
+ return readCanonicalSkillFiles(skillsDir).map((skill) => {
17046
+ const metadata = getCanonicalSkillMetadata(skill);
17047
+ return {
17048
+ dirName: normalizeRelativePath(skill.dirName),
17049
+ title: metadata.title,
17050
+ description: metadata.description,
17051
+ toolNames: [],
17052
+ path: normalizeRelativePath(relative10(rootDir, skill.filePath)),
17053
+ ...metadata.supportPaths.length > 0 ? { supportPaths: metadata.supportPaths } : {}
17054
+ };
17055
+ });
16311
17056
  }
16312
17057
  function summarizeDiscoveryDescription(description, trailing) {
16313
17058
  const base = description?.replace(/\s+/g, " ").trim().slice(0, 180);
@@ -16823,7 +17568,7 @@ ${additions.map((block) => `- ${block.replace(/\n/g, "\n ")}`).join("\n")}
16823
17568
  }
16824
17569
 
16825
17570
  // src/cli/doctor.ts
16826
- import { spawn as spawn3 } from "child_process";
17571
+ import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
16827
17572
  import { accessSync, constants, existsSync as existsSync23, lstatSync as lstatSync3, readFileSync as readFileSync13, readdirSync as readdirSync10 } from "fs";
16828
17573
  import { basename as basename6, dirname as dirname7, resolve as resolve19 } from "path";
16829
17574
 
@@ -16876,7 +17621,11 @@ function listHookCommands(hooks) {
16876
17621
  return commands;
16877
17622
  }
16878
17623
  function planInstallUserConfig(config, platforms = config.targets) {
16879
- const entries = collectUserConfigEntries(config, platforms);
17624
+ const baseEntries = collectUserConfigEntries(config, platforms);
17625
+ const entries = [
17626
+ ...baseEntries,
17627
+ ...collectNativeMcpAuthUserConfigEntries(config, platforms, baseEntries)
17628
+ ];
16880
17629
  return entries.map((field) => {
16881
17630
  const envVar = field.envVar ?? defaultUserConfigEnvVar(field.key);
16882
17631
  const envValue = process.env[envVar];
@@ -17330,7 +18079,10 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
17330
18079
  type: server.transport === "sse" ? "sse" : "http",
17331
18080
  url: server.url
17332
18081
  };
17333
- if (!usesPlatformManagedAuth && server.auth?.type === "bearer" && server.auth.envVar && env[server.auth.envVar]) {
18082
+ const nativeHeaders = getNativeJsonHeadersOverride(config, platform, name);
18083
+ if (!usesPlatformManagedAuth && nativeHeaders) {
18084
+ entry.headers = materializeEnvRecord(nativeHeaders, env);
18085
+ } else if (!usesPlatformManagedAuth && server.auth?.type === "bearer" && server.auth.envVar && env[server.auth.envVar]) {
17334
18086
  entry.headers = {
17335
18087
  Authorization: `Bearer ${env[server.auth.envVar]}`
17336
18088
  };
@@ -17360,7 +18112,32 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
17360
18112
  const entry = {
17361
18113
  url: server.url
17362
18114
  };
17363
- if (server.auth?.type === "bearer" && server.auth.envVar && env[server.auth.envVar]) {
18115
+ const nativeOverride = getNativeCodexMcpEntryOverride(config, name);
18116
+ if (nativeOverride) {
18117
+ if (typeof nativeOverride.bearer_token_env_var === "string") {
18118
+ const envVar = nativeOverride.bearer_token_env_var;
18119
+ if (env[envVar]) {
18120
+ entry.bearer_token_env_var = envVar;
18121
+ }
18122
+ }
18123
+ if (nativeOverride.auth) {
18124
+ entry.auth = nativeOverride.auth;
18125
+ }
18126
+ if (nativeOverride.env_http_headers && typeof nativeOverride.env_http_headers === "object") {
18127
+ entry.http_headers = Object.fromEntries(
18128
+ Object.entries(nativeOverride.env_http_headers).filter(([, value]) => typeof value === "string").map(([headerName, envVar]) => [
18129
+ headerName,
18130
+ env[envVar] ?? `\${${envVar}}`
18131
+ ])
18132
+ );
18133
+ }
18134
+ if (nativeOverride.http_headers && typeof nativeOverride.http_headers === "object") {
18135
+ entry.http_headers = materializeEnvRecord(
18136
+ nativeOverride.http_headers,
18137
+ env
18138
+ );
18139
+ }
18140
+ } else if (server.auth?.type === "bearer" && server.auth.envVar && env[server.auth.envVar]) {
17364
18141
  entry.http_headers = {
17365
18142
  Authorization: `Bearer ${env[server.auth.envVar]}`
17366
18143
  };
@@ -18801,6 +19578,166 @@ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
18801
19578
  path: layout.manifestPath
18802
19579
  });
18803
19580
  }
19581
+ function parseInstalledPermissionRules(scriptSource) {
19582
+ const match = scriptSource.match(/const RULES = (\[[\s\S]*?\]);\nconst ACTION_PRIORITY/);
19583
+ if (!match?.[1]) {
19584
+ throw new Error("Could not locate embedded RULES payload in hooks/pluxx-permissions.mjs.");
19585
+ }
19586
+ const parsed = JSON.parse(match[1]);
19587
+ if (!Array.isArray(parsed)) {
19588
+ throw new Error("Embedded RULES payload is not an array.");
19589
+ }
19590
+ return parsed;
19591
+ }
19592
+ function materializePermissionPattern(pattern) {
19593
+ return pattern.replace(/\*\*/g, "probe/path").replace(/\*/g, "probe");
19594
+ }
19595
+ function buildInstalledPermissionProbe(platform, rule) {
19596
+ const value = materializePermissionPattern(rule.pattern);
19597
+ if (rule.kind === "Bash") {
19598
+ return {
19599
+ action: rule.action,
19600
+ raw: rule.raw,
19601
+ mode: platform === "cursor" ? "cursor-shell" : "claude-pretool",
19602
+ event: platform === "cursor" ? { command: value } : { tool_name: "Bash", tool_input: { command: value } }
19603
+ };
19604
+ }
19605
+ if (rule.kind === "Read") {
19606
+ return {
19607
+ action: rule.action,
19608
+ raw: rule.raw,
19609
+ mode: platform === "cursor" ? "cursor-read" : "claude-pretool",
19610
+ event: platform === "cursor" ? { file_path: value } : { tool_name: "Read", tool_input: { file_path: value } }
19611
+ };
19612
+ }
19613
+ if (rule.kind === "Edit") {
19614
+ return {
19615
+ action: rule.action,
19616
+ raw: rule.raw,
19617
+ mode: platform === "cursor" ? "cursor" : "claude-pretool",
19618
+ event: { tool_name: "Edit", tool_input: { file_path: value } }
19619
+ };
19620
+ }
19621
+ if (rule.kind === "Skill") {
19622
+ return {
19623
+ action: rule.action,
19624
+ raw: rule.raw,
19625
+ mode: platform === "cursor" ? "cursor" : "claude-pretool",
19626
+ event: { tool_name: "Skill", tool_input: { name: value } }
19627
+ };
19628
+ }
19629
+ if (rule.kind === "MCP") {
19630
+ const canonical = value.includes(".") ? value : `${value}.probe`;
19631
+ const dotIndex = canonical.indexOf(".");
19632
+ const server = canonical.slice(0, dotIndex).trim();
19633
+ const tool = canonical.slice(dotIndex + 1).trim();
19634
+ if (!server || !tool) return null;
19635
+ return {
19636
+ action: rule.action,
19637
+ raw: rule.raw,
19638
+ mode: platform === "cursor" ? "cursor-mcp" : "claude-pretool",
19639
+ event: platform === "cursor" ? { tool_name: `mcp__${server}__${tool.replace(/\./g, "__")}` } : { tool_name: `mcp__${server}__${tool.replace(/\./g, "__")}` }
19640
+ };
19641
+ }
19642
+ return null;
19643
+ }
19644
+ function extractPermissionDecision(platform, payload) {
19645
+ if (platform === "claude-code") {
19646
+ const hookSpecificOutput = payload.hookSpecificOutput;
19647
+ if (!hookSpecificOutput || typeof hookSpecificOutput !== "object") return void 0;
19648
+ const decision2 = hookSpecificOutput.permissionDecision;
19649
+ return typeof decision2 === "string" ? decision2 : void 0;
19650
+ }
19651
+ const decision = payload.permission;
19652
+ return typeof decision === "string" ? decision : void 0;
19653
+ }
19654
+ function smokeCheckInstalledPermissionHook(rootDir, layout) {
19655
+ if (layout.platform !== "claude-code" && layout.platform !== "cursor") {
19656
+ return null;
19657
+ }
19658
+ const scriptPath = resolve19(rootDir, "hooks/pluxx-permissions.mjs");
19659
+ if (!existsSync23(scriptPath)) {
19660
+ return null;
19661
+ }
19662
+ const rules = parseInstalledPermissionRules(readFileSync13(scriptPath, "utf-8"));
19663
+ const selected = /* @__PURE__ */ new Map();
19664
+ for (const rule of rules) {
19665
+ if (selected.has(rule.action)) continue;
19666
+ const probe = buildInstalledPermissionProbe(layout.platform, rule);
19667
+ if (!probe) continue;
19668
+ selected.set(rule.action, probe);
19669
+ }
19670
+ if (selected.size === 0) {
19671
+ return {
19672
+ ok: false,
19673
+ detail: "Permission hook script is present, but no supported canonical probe cases could be derived from its embedded rules."
19674
+ };
19675
+ }
19676
+ const failures = [];
19677
+ const successes = [];
19678
+ for (const probe of selected.values()) {
19679
+ const result = spawnSync2("node", [scriptPath, probe.mode], {
19680
+ cwd: rootDir,
19681
+ input: JSON.stringify(probe.event),
19682
+ encoding: "utf-8",
19683
+ env: process.env
19684
+ });
19685
+ if (result.status !== 0) {
19686
+ failures.push(`${probe.raw} exited ${result.status ?? "null"}${result.stderr ? `: ${result.stderr.trim()}` : ""}`);
19687
+ continue;
19688
+ }
19689
+ let payload;
19690
+ try {
19691
+ payload = JSON.parse(result.stdout || "{}");
19692
+ } catch (error) {
19693
+ failures.push(`${probe.raw} returned non-JSON output: ${error instanceof Error ? error.message : String(error)}`);
19694
+ continue;
19695
+ }
19696
+ const decision = extractPermissionDecision(layout.platform, payload);
19697
+ if (decision !== probe.action) {
19698
+ failures.push(`${probe.raw} returned ${decision ?? "no decision"} instead of ${probe.action}`);
19699
+ continue;
19700
+ }
19701
+ successes.push(`${probe.raw} -> ${decision}`);
19702
+ }
19703
+ return failures.length > 0 ? { ok: false, detail: failures.join("; ") } : { ok: true, detail: `Validated ${successes.join(", ")}` };
19704
+ }
19705
+ function checkInstalledPermissionHook(checks, rootDir, layout) {
19706
+ const permissionScriptPath = "hooks/pluxx-permissions.mjs";
19707
+ let scriptResult;
19708
+ try {
19709
+ scriptResult = smokeCheckInstalledPermissionHook(rootDir, layout);
19710
+ } catch (error) {
19711
+ addCheck2(checks, {
19712
+ level: "error",
19713
+ code: "consumer-permission-hook-invalid",
19714
+ title: "Installed bundled permission hook behavior is broken",
19715
+ detail: error instanceof Error ? error.message : String(error),
19716
+ fix: "Rebuild or reinstall the plugin so the bundled permission hook script and hook wiring stay aligned with the declared permission rules.",
19717
+ path: permissionScriptPath
19718
+ });
19719
+ return;
19720
+ }
19721
+ if (!scriptResult) {
19722
+ addCheck2(checks, {
19723
+ level: "info",
19724
+ code: "consumer-permission-hook-not-applicable",
19725
+ title: "No bundled permission hook smoke check for this install",
19726
+ detail: layout.platform === "claude-code" || layout.platform === "cursor" ? "This installed bundle does not include hooks/pluxx-permissions.mjs." : "This platform does not currently use a bundled pluxx-permissions.mjs hook script for permission enforcement.",
19727
+ fix: "No action needed unless you expected bundled permission-hook enforcement in this installed bundle.",
19728
+ path: permissionScriptPath
19729
+ });
19730
+ return;
19731
+ }
19732
+ addCheck2(checks, {
19733
+ level: scriptResult.ok ? "success" : "error",
19734
+ code: scriptResult.ok ? "consumer-permission-hook-valid" : "consumer-permission-hook-invalid",
19735
+ title: scriptResult.ok ? "Installed bundled permission hook returns expected decisions" : "Installed bundled permission hook behavior is broken",
19736
+ detail: scriptResult.detail,
19737
+ fix: scriptResult.ok ? "No action needed." : "Rebuild or reinstall the plugin so the bundled permission hook script and hook wiring stay aligned with the declared permission rules.",
19738
+ path: permissionScriptPath
19739
+ });
19740
+ }
18804
19741
  function findMissingInstalledStdioRuntimePaths(rootDir, stdioEntries) {
18805
19742
  const missing = /* @__PURE__ */ new Set();
18806
19743
  for (const server of stdioEntries) {
@@ -19068,6 +20005,7 @@ async function doctorConsumer(rootDir = process.cwd()) {
19068
20005
  });
19069
20006
  checkConsumerManifest(checks, rootDir, layout);
19070
20007
  checkInstalledBundleIntegrity(checks, rootDir, layout);
20008
+ checkInstalledPermissionHook(checks, rootDir, layout);
19071
20009
  checkInstalledUserConfig(checks, rootDir);
19072
20010
  checkInstalledEnvValidation(checks, rootDir);
19073
20011
  checkInstalledRuntimeScriptRoles(checks, rootDir);
@@ -19262,7 +20200,7 @@ async function runBuild(rootDir, targets) {
19262
20200
  }
19263
20201
 
19264
20202
  // src/cli/migrate.ts
19265
- import { basename as basename7, relative as relative12, resolve as resolve21 } from "path";
20203
+ import { basename as basename7, dirname as dirname8, relative as relative12, resolve as resolve21 } from "path";
19266
20204
  import { existsSync as existsSync24, readdirSync as readdirSync11, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
19267
20205
  function detectPlatform(pluginDir) {
19268
20206
  const checks = [
@@ -19291,11 +20229,19 @@ function detectPlatform(pluginDir) {
19291
20229
  } catch {
19292
20230
  }
19293
20231
  }
20232
+ if (existsSync24(resolve21(pluginDir, "CLAUDE.md"))) {
20233
+ return { platform: "claude-code" };
20234
+ }
19294
20235
  return null;
19295
20236
  }
19296
20237
  function parseManifest(detection) {
20238
+ if (!detection.manifestPath) {
20239
+ return {};
20240
+ }
19297
20241
  const raw = JSON.parse(readFileSync14(detection.manifestPath, "utf-8"));
19298
20242
  const result = {};
20243
+ const brand = {};
20244
+ const platforms = {};
19299
20245
  if (raw.name) result.name = raw.name;
19300
20246
  if (raw.version) result.version = raw.version;
19301
20247
  if (raw.description) result.description = raw.description;
@@ -19315,169 +20261,730 @@ function parseManifest(detection) {
19315
20261
  };
19316
20262
  }
19317
20263
  }
20264
+ const homepage = typeof raw.homepage === "string" ? raw.homepage : void 0;
20265
+ if (homepage) brand.websiteURL = homepage;
20266
+ if (typeof raw.icon === "string") brand.icon = raw.icon;
20267
+ if (typeof raw.logo === "string") {
20268
+ brand.logo = raw.logo;
20269
+ if (!brand.icon) brand.icon = raw.logo;
20270
+ }
20271
+ const codexInterface = asRecord4(raw.interface);
20272
+ if (codexInterface) {
20273
+ if (typeof codexInterface.displayName === "string") brand.displayName = codexInterface.displayName;
20274
+ if (typeof codexInterface.shortDescription === "string") brand.shortDescription = codexInterface.shortDescription;
20275
+ if (typeof codexInterface.longDescription === "string") brand.longDescription = codexInterface.longDescription;
20276
+ if (typeof codexInterface.category === "string") brand.category = codexInterface.category;
20277
+ if (typeof codexInterface.brandColor === "string") brand.color = codexInterface.brandColor;
20278
+ if (typeof codexInterface.composerIcon === "string") brand.icon = codexInterface.composerIcon;
20279
+ if (typeof codexInterface.logo === "string") brand.logo = codexInterface.logo;
20280
+ if (Array.isArray(codexInterface.defaultPrompt)) {
20281
+ const prompts = codexInterface.defaultPrompt.filter((value) => typeof value === "string");
20282
+ if (prompts.length > 0) brand.defaultPrompts = prompts;
20283
+ }
20284
+ if (typeof codexInterface.websiteURL === "string") brand.websiteURL = codexInterface.websiteURL;
20285
+ if (typeof codexInterface.privacyPolicyURL === "string") brand.privacyPolicyURL = codexInterface.privacyPolicyURL;
20286
+ if (typeof codexInterface.termsOfServiceURL === "string") brand.termsOfServiceURL = codexInterface.termsOfServiceURL;
20287
+ if (Array.isArray(codexInterface.screenshots)) {
20288
+ const screenshots = codexInterface.screenshots.filter((value) => typeof value === "string");
20289
+ if (screenshots.length > 0) brand.screenshots = screenshots;
20290
+ }
20291
+ const remainingInterface = Object.fromEntries(
20292
+ Object.entries(codexInterface).filter(([key]) => !(/* @__PURE__ */ new Set([
20293
+ "displayName",
20294
+ "shortDescription",
20295
+ "longDescription",
20296
+ "category",
20297
+ "brandColor",
20298
+ "composerIcon",
20299
+ "logo",
20300
+ "defaultPrompt",
20301
+ "websiteURL",
20302
+ "privacyPolicyURL",
20303
+ "termsOfServiceURL",
20304
+ "screenshots"
20305
+ ])).has(key))
20306
+ );
20307
+ if (Object.keys(remainingInterface).length > 0) {
20308
+ platforms.codex = {
20309
+ ...platforms.codex ?? {},
20310
+ interface: remainingInterface
20311
+ };
20312
+ }
20313
+ }
20314
+ if (detection.platform === "cursor") {
20315
+ const marketplacePath = resolve21(dirname8(detection.manifestPath), "marketplace.json");
20316
+ if (existsSync24(marketplacePath)) {
20317
+ try {
20318
+ const marketplace = JSON.parse(readFileSync14(marketplacePath, "utf-8"));
20319
+ if (marketplace && typeof marketplace === "object") {
20320
+ platforms.cursor = {
20321
+ ...platforms.cursor ?? {},
20322
+ marketplace
20323
+ };
20324
+ }
20325
+ } catch {
20326
+ }
20327
+ }
20328
+ }
20329
+ if (detection.platform === "codex") {
20330
+ const appPath = resolve21(dirname8(dirname8(detection.manifestPath)), ".app.json");
20331
+ if (existsSync24(appPath)) {
20332
+ try {
20333
+ const app = JSON.parse(readFileSync14(appPath, "utf-8"));
20334
+ if (app && typeof app === "object" && !Array.isArray(app)) {
20335
+ platforms.codex = {
20336
+ ...platforms.codex ?? {},
20337
+ app
20338
+ };
20339
+ }
20340
+ } catch {
20341
+ }
20342
+ }
20343
+ }
20344
+ if (detection.platform === "opencode") {
20345
+ const pluginConfig = asRecord4(raw.plugin);
20346
+ if (pluginConfig) {
20347
+ platforms.opencode = {
20348
+ ...platforms.opencode ?? {},
20349
+ plugin: pluginConfig
20350
+ };
20351
+ }
20352
+ }
20353
+ if (Object.keys(brand).length > 0) result.brand = brand;
20354
+ if (Object.keys(platforms).length > 0) result.platforms = platforms;
19318
20355
  return result;
19319
20356
  }
19320
20357
  function parseMcp(pluginDir, detection) {
19321
- const mcpPaths = [
19322
- resolve21(pluginDir, ".mcp.json"),
19323
- resolve21(pluginDir, "mcp.json")
20358
+ const mcpCandidates = [
20359
+ { path: resolve21(pluginDir, ".mcp.json"), parser: "json" },
20360
+ { path: resolve21(pluginDir, "mcp.json"), parser: "json" }
19324
20361
  ];
19325
- try {
19326
- const manifest = JSON.parse(readFileSync14(detection.manifestPath, "utf-8"));
19327
- if (manifest.mcpServers && typeof manifest.mcpServers === "string") {
19328
- mcpPaths.unshift(resolve21(pluginDir, manifest.mcpServers));
20362
+ if (detection.platform === "codex") {
20363
+ mcpCandidates.push({ path: resolve21(pluginDir, ".codex/config.toml"), parser: "toml" });
20364
+ }
20365
+ if (detection.platform === "opencode") {
20366
+ mcpCandidates.push({ path: resolve21(pluginDir, "opencode.json"), parser: "json" });
20367
+ mcpCandidates.push({ path: resolve21(pluginDir, ".opencode.json"), parser: "json" });
20368
+ }
20369
+ if (detection.manifestPath) {
20370
+ try {
20371
+ const manifest = JSON.parse(readFileSync14(detection.manifestPath, "utf-8"));
20372
+ if (manifest.mcpServers && typeof manifest.mcpServers === "string") {
20373
+ mcpCandidates.unshift({ path: resolve21(pluginDir, manifest.mcpServers), parser: "json" });
20374
+ }
20375
+ } catch {
20376
+ }
20377
+ }
20378
+ const mergedServers = {};
20379
+ const mergedWarnings = /* @__PURE__ */ new Set();
20380
+ let runtimeAuthMode = "inline";
20381
+ let platformOverrides;
20382
+ for (const candidate of mcpCandidates) {
20383
+ if (!existsSync24(candidate.path)) continue;
20384
+ const parsed = candidate.parser === "toml" ? parseTomlMcpFile(candidate.path, detection.platform) : parseJsonMcpFile(candidate.path, detection.platform);
20385
+ if (Object.keys(parsed.servers).length === 0) continue;
20386
+ for (const [serverName, server] of Object.entries(parsed.servers)) {
20387
+ const previous = mergedServers[serverName];
20388
+ const combinedWarnings = [.../* @__PURE__ */ new Set([...previous?.warnings ?? [], ...server.warnings ?? []])];
20389
+ mergedServers[serverName] = {
20390
+ ...previous ?? {},
20391
+ ...server,
20392
+ ...combinedWarnings.length > 0 ? { warnings: combinedWarnings } : {}
20393
+ };
20394
+ }
20395
+ if (parsed.runtimeAuthMode === "platform") {
20396
+ runtimeAuthMode = "platform";
20397
+ }
20398
+ for (const warning of parsed.warnings) {
20399
+ mergedWarnings.add(warning);
20400
+ }
20401
+ platformOverrides = mergePlatformOverrideMaps(platformOverrides, parsed.platformOverrides);
20402
+ }
20403
+ if (Object.keys(mergedServers).length > 0) {
20404
+ return {
20405
+ servers: mergedServers,
20406
+ runtimeAuthMode,
20407
+ warnings: [...mergedWarnings],
20408
+ ...platformOverrides ? { platformOverrides } : {}
20409
+ };
20410
+ }
20411
+ return {
20412
+ servers: {},
20413
+ runtimeAuthMode: "inline",
20414
+ warnings: []
20415
+ };
20416
+ }
20417
+ var HOOK_EVENT_MAP = {
20418
+ SessionStart: "sessionStart",
20419
+ SessionEnd: "sessionEnd",
20420
+ PreToolUse: "preToolUse",
20421
+ PostToolUse: "postToolUse",
20422
+ UserPromptSubmit: "beforeSubmitPrompt",
20423
+ BeforeShellExecution: "beforeShellExecution",
20424
+ AfterShellExecution: "afterShellExecution",
20425
+ BeforeMCPExecution: "beforeMCPExecution",
20426
+ AfterMCPExecution: "afterMCPExecution",
20427
+ AfterFileEdit: "afterFileEdit",
20428
+ BeforeReadFile: "beforeReadFile",
20429
+ BeforeSubmitPrompt: "beforeSubmitPrompt",
20430
+ Stop: "stop",
20431
+ // Also handle already-normalized names
20432
+ sessionStart: "sessionStart",
20433
+ sessionEnd: "sessionEnd",
20434
+ preToolUse: "preToolUse",
20435
+ postToolUse: "postToolUse"
20436
+ };
20437
+ function parseHooks(pluginDir, detection) {
20438
+ const hooksPaths = [
20439
+ resolve21(pluginDir, ".codex", "hooks.json"),
20440
+ resolve21(pluginDir, "hooks.json"),
20441
+ resolve21(pluginDir, "hooks", "hooks.json")
20442
+ ];
20443
+ if (detection.manifestPath) {
20444
+ try {
20445
+ const manifest = JSON.parse(readFileSync14(detection.manifestPath, "utf-8"));
20446
+ if (manifest.hooks && typeof manifest.hooks === "string") {
20447
+ hooksPaths.unshift(resolve21(pluginDir, manifest.hooks));
20448
+ }
20449
+ } catch {
19329
20450
  }
19330
- } catch {
19331
20451
  }
19332
- for (const mcpPath of mcpPaths) {
19333
- if (!existsSync24(mcpPath)) continue;
20452
+ for (const hooksPath of hooksPaths) {
20453
+ if (!existsSync24(hooksPath)) continue;
19334
20454
  try {
19335
- const raw = JSON.parse(readFileSync14(mcpPath, "utf-8"));
19336
- const servers = raw.mcpServers ?? raw;
19337
- if (!servers || typeof servers !== "object") continue;
20455
+ const raw = JSON.parse(readFileSync14(hooksPath, "utf-8"));
20456
+ const hooksObj = raw.hooks ?? raw;
20457
+ if (!hooksObj || typeof hooksObj !== "object") continue;
19338
20458
  const result = {};
19339
- for (const [name, config] of Object.entries(servers)) {
19340
- const cfg = config;
19341
- const entry = {};
19342
- if (cfg.url) entry.url = cfg.url;
19343
- if (cfg.type === "stdio" || cfg.command) {
19344
- entry.transport = "stdio";
19345
- if (cfg.command) entry.command = cfg.command;
19346
- if (cfg.args) entry.args = cfg.args;
19347
- if (cfg.env) entry.env = cfg.env;
19348
- } else if (cfg.type === "sse") {
19349
- entry.transport = "sse";
19350
- } else {
19351
- entry.transport = "http";
19352
- }
19353
- if (cfg.headers && typeof cfg.headers === "object") {
19354
- const headers = cfg.headers;
19355
- const authHeader = headers["Authorization"] ?? headers["authorization"];
19356
- if (authHeader) {
19357
- const envMatch = authHeader.match(/\$\{(\w+)\}/);
19358
- if (envMatch) {
19359
- entry.auth = {
19360
- type: "bearer",
19361
- envVar: envMatch[1],
19362
- headerTemplate: authHeader.replace(/\$\{\w+\}/, "${value}")
19363
- };
20459
+ for (const [event, entries] of Object.entries(hooksObj)) {
20460
+ const normalizedEvent = HOOK_EVENT_MAP[event] ?? event;
20461
+ const hookEntries = [];
20462
+ if (!Array.isArray(entries)) continue;
20463
+ for (const entry of entries) {
20464
+ if (entry.hooks && Array.isArray(entry.hooks)) {
20465
+ for (const hook of entry.hooks) {
20466
+ const parsedHook = parseMigratedHookEntry(hook, entry.matcher);
20467
+ if (parsedHook) hookEntries.push(parsedHook);
20468
+ }
20469
+ } else {
20470
+ const parsedHook = parseMigratedHookEntry(entry);
20471
+ if (parsedHook) {
20472
+ hookEntries.push(parsedHook);
19364
20473
  }
19365
20474
  }
19366
20475
  }
19367
- if (cfg.bearer_token_env_var) {
19368
- entry.auth = {
19369
- type: "bearer",
19370
- envVar: cfg.bearer_token_env_var
19371
- };
20476
+ if (hookEntries.length > 0) {
20477
+ result[normalizedEvent] = hookEntries;
19372
20478
  }
19373
- if (cfg.env_http_headers && typeof cfg.env_http_headers === "object") {
19374
- const envHeaders = Object.entries(cfg.env_http_headers);
19375
- if (envHeaders.length > 0) {
19376
- const [headerName, envVar] = envHeaders[0];
19377
- entry.auth = {
19378
- type: "header",
19379
- envVar,
19380
- headerName,
19381
- headerTemplate: "${value}"
19382
- };
19383
- }
20479
+ }
20480
+ return result;
20481
+ } catch {
20482
+ continue;
20483
+ }
20484
+ }
20485
+ return {};
20486
+ }
20487
+ function parseMigratedHookEntry(raw, fallbackMatcher) {
20488
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
20489
+ const candidate = raw;
20490
+ const inferredType = candidate.type;
20491
+ const type = inferredType === "command" || inferredType === "http" || inferredType === "mcp_tool" || inferredType === "prompt" || inferredType === "agent" ? inferredType : typeof candidate.prompt === "string" ? "prompt" : typeof candidate.url === "string" ? "http" : typeof candidate.server === "string" || typeof candidate.tool === "string" ? "mcp_tool" : "command";
20492
+ const entry = { type };
20493
+ if (typeof candidate.command === "string") entry.command = candidate.command;
20494
+ if (typeof candidate.prompt === "string") entry.prompt = candidate.prompt;
20495
+ if (typeof candidate.model === "string") entry.model = candidate.model;
20496
+ if (typeof candidate.url === "string") entry.url = candidate.url;
20497
+ if (candidate.headers && typeof candidate.headers === "object" && !Array.isArray(candidate.headers)) {
20498
+ entry.headers = Object.fromEntries(
20499
+ Object.entries(candidate.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
20500
+ );
20501
+ }
20502
+ if (Array.isArray(candidate.allowedEnvVars) && candidate.allowedEnvVars.every((value) => typeof value === "string")) {
20503
+ entry.allowedEnvVars = candidate.allowedEnvVars;
20504
+ }
20505
+ if (typeof candidate.server === "string") entry.server = candidate.server;
20506
+ if (typeof candidate.tool === "string") entry.tool = candidate.tool;
20507
+ if (candidate.input && typeof candidate.input === "object" && !Array.isArray(candidate.input)) {
20508
+ entry.input = candidate.input;
20509
+ }
20510
+ if (typeof candidate.if === "string") entry.if = candidate.if;
20511
+ if (typeof candidate.async === "boolean") entry.async = candidate.async;
20512
+ if (typeof candidate.asyncRewake === "boolean") entry.asyncRewake = candidate.asyncRewake;
20513
+ if (candidate.shell === "bash") entry.shell = "bash";
20514
+ if (typeof candidate.timeout === "number") entry.timeout = candidate.timeout;
20515
+ const matcher = candidate.matcher ?? fallbackMatcher;
20516
+ if (typeof matcher === "string") {
20517
+ entry.matcher = matcher;
20518
+ } else if (matcher && typeof matcher === "object" && !Array.isArray(matcher)) {
20519
+ entry.matcher = matcher;
20520
+ }
20521
+ if (typeof candidate.failClosed === "boolean") entry.failClosed = candidate.failClosed;
20522
+ if (typeof candidate.loop_limit === "number" || candidate.loop_limit === null) {
20523
+ entry.loop_limit = candidate.loop_limit;
20524
+ }
20525
+ if (type === "command" && !entry.command) return null;
20526
+ if (type === "http" && !entry.url) return null;
20527
+ if (type === "mcp_tool" && (!entry.server || !entry.tool)) return null;
20528
+ if ((type === "prompt" || type === "agent") && !entry.prompt) return null;
20529
+ return entry;
20530
+ }
20531
+ function findInstructions(pluginDir) {
20532
+ const candidates = [
20533
+ "CLAUDE.md",
20534
+ "AGENTS.md",
20535
+ "instructions.md",
20536
+ "INSTRUCTIONS.md",
20537
+ "README.md"
20538
+ ];
20539
+ for (const candidate of candidates) {
20540
+ const filePath = resolve21(pluginDir, candidate);
20541
+ if (existsSync24(filePath)) {
20542
+ return `./${candidate}`;
20543
+ }
20544
+ }
20545
+ return void 0;
20546
+ }
20547
+ function parseCursorRules(pluginDir, detection) {
20548
+ if (detection.platform !== "cursor") return [];
20549
+ const manifest = detection.manifestPath ? tryReadJson(detection.manifestPath) : null;
20550
+ const configuredRulesDir = typeof manifest?.rules === "string" ? stripRelativePrefix(manifest.rules) : void 0;
20551
+ const candidates = [
20552
+ configuredRulesDir,
20553
+ "rules",
20554
+ ".cursor/rules"
20555
+ ].filter((value) => Boolean(value));
20556
+ for (const relativeDir of candidates) {
20557
+ const dirPath = resolve21(pluginDir, relativeDir);
20558
+ if (!existsSync24(dirPath)) continue;
20559
+ const entries = collectFiles(dirPath, (entry) => entry.endsWith(".mdc")).map((entry) => parseCursorRuleFile(pluginDir, entry)).filter((entry) => Boolean(entry));
20560
+ if (entries.length > 0) return entries;
20561
+ }
20562
+ return [];
20563
+ }
20564
+ function parseCursorRuleFile(pluginDir, filePath) {
20565
+ const content = readFileSync14(filePath, "utf-8");
20566
+ const { hasFrontmatter, frontmatterLines, body } = splitMarkdownFrontmatter5(content);
20567
+ if (!hasFrontmatter) return null;
20568
+ const description = parseTopLevelStringFrontmatter(frontmatterLines, "description") ?? titleCaseFromDirName(basename7(filePath, ".mdc"));
20569
+ const globs = parseTopLevelStringOrStringArrayFrontmatter(frontmatterLines, "globs");
20570
+ const alwaysApply = parseTopLevelBooleanFrontmatter(frontmatterLines, "alwaysApply");
20571
+ return {
20572
+ description,
20573
+ ...globs !== void 0 ? { globs } : {},
20574
+ ...alwaysApply !== void 0 ? { alwaysApply } : {},
20575
+ ...body.trim() ? { content: body.trim() } : {},
20576
+ path: `./${relative12(pluginDir, filePath).replace(/\\/g, "/")}`
20577
+ };
20578
+ }
20579
+ function collectFiles(dir, predicate) {
20580
+ const results = [];
20581
+ const stack = [dir];
20582
+ while (stack.length > 0) {
20583
+ const current = stack.pop();
20584
+ for (const entry of readdirSync11(current, { withFileTypes: true })) {
20585
+ const fullPath = resolve21(current, entry.name);
20586
+ if (entry.isDirectory()) {
20587
+ stack.push(fullPath);
20588
+ continue;
20589
+ }
20590
+ if (entry.isFile() && predicate(entry.name)) {
20591
+ results.push(fullPath);
20592
+ }
20593
+ }
20594
+ }
20595
+ return results.sort((a, b) => a.localeCompare(b));
20596
+ }
20597
+ function findOpenCodeConfiguredInstructions(pluginDir) {
20598
+ const candidates = [resolve21(pluginDir, "opencode.json"), resolve21(pluginDir, ".opencode.json")];
20599
+ const discovered = /* @__PURE__ */ new Set();
20600
+ for (const path of candidates) {
20601
+ const raw = tryReadJson(path);
20602
+ if (!raw) continue;
20603
+ const instructions = raw.instructions;
20604
+ const values = typeof instructions === "string" ? [instructions] : Array.isArray(instructions) ? instructions.filter((value) => typeof value === "string") : [];
20605
+ for (const value of values) {
20606
+ if (value.startsWith("./") || value.startsWith("../")) {
20607
+ const normalized = `./${stripRelativePrefix(value)}`;
20608
+ if (existsSync24(resolve21(pluginDir, normalized))) {
20609
+ discovered.add(normalized);
20610
+ }
20611
+ }
20612
+ }
20613
+ }
20614
+ return [...discovered];
20615
+ }
20616
+ function findInstructionSources(pluginDir, detection) {
20617
+ const primary = findInstructions(pluginDir);
20618
+ const extraPaths = /* @__PURE__ */ new Set();
20619
+ const platformOverrides = {};
20620
+ if (detection.platform === "cursor") {
20621
+ const nestedAgents = collectFiles(pluginDir, (entry) => entry === "AGENTS.md").map((path) => `./${relative12(pluginDir, path).replace(/\\/g, "/")}`).filter((path) => path !== primary);
20622
+ if (nestedAgents.length > 0) {
20623
+ platformOverrides.cursor = {
20624
+ ...platformOverrides.cursor ?? {},
20625
+ instructionSources: {
20626
+ ...asRecord4(platformOverrides.cursor?.instructionSources) ?? {},
20627
+ nestedAgents
20628
+ }
20629
+ };
20630
+ for (const path of nestedAgents) extraPaths.add(path);
20631
+ }
20632
+ }
20633
+ if (detection.platform === "codex") {
20634
+ const overridePath = "./AGENTS.override.md";
20635
+ if (existsSync24(resolve21(pluginDir, stripRelativePrefix(overridePath)))) {
20636
+ platformOverrides.codex = {
20637
+ ...platformOverrides.codex ?? {},
20638
+ instructionSources: {
20639
+ ...asRecord4(platformOverrides.codex?.instructionSources) ?? {},
20640
+ override: overridePath
20641
+ }
20642
+ };
20643
+ if (overridePath !== primary) {
20644
+ extraPaths.add(overridePath);
20645
+ }
20646
+ }
20647
+ }
20648
+ if (detection.platform === "opencode") {
20649
+ const configured = findOpenCodeConfiguredInstructions(pluginDir);
20650
+ if (configured.length > 0) {
20651
+ platformOverrides.opencode = {
20652
+ ...platformOverrides.opencode ?? {},
20653
+ instructionSources: {
20654
+ ...asRecord4(platformOverrides.opencode?.instructionSources) ?? {},
20655
+ configured
20656
+ }
20657
+ };
20658
+ for (const path of configured) {
20659
+ if (path !== primary) extraPaths.add(path);
20660
+ }
20661
+ }
20662
+ }
20663
+ return {
20664
+ primary: primary ?? (detection.platform === "opencode" ? findOpenCodeConfiguredInstructions(pluginDir)[0] : void 0),
20665
+ extraPaths: [...extraPaths].sort(),
20666
+ ...Object.keys(platformOverrides).length > 0 ? { platformOverrides } : {}
20667
+ };
20668
+ }
20669
+ function collectPackageEntryPaths(value, acc) {
20670
+ if (typeof value === "string") {
20671
+ if (value.startsWith("./") || value.startsWith("../")) {
20672
+ acc.add(`./${stripRelativePrefix(value)}`);
20673
+ }
20674
+ return;
20675
+ }
20676
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
20677
+ for (const nested of Object.values(value)) {
20678
+ collectPackageEntryPaths(nested, acc);
20679
+ }
20680
+ }
20681
+ function findOpenCodeDistributionSources(pluginDir, detection) {
20682
+ if (detection.platform !== "opencode" || !detection.manifestPath) {
20683
+ return { extraPaths: [] };
20684
+ }
20685
+ const pkg = tryReadJson(detection.manifestPath);
20686
+ if (!pkg) return { extraPaths: [] };
20687
+ const entrypoints = {};
20688
+ const extraPaths = /* @__PURE__ */ new Set();
20689
+ for (const key of ["main", "module", "types"]) {
20690
+ const value = pkg[key];
20691
+ if (typeof value === "string") {
20692
+ entrypoints[key] = value;
20693
+ collectPackageEntryPaths(value, extraPaths);
20694
+ }
20695
+ }
20696
+ if (pkg.exports !== void 0) {
20697
+ entrypoints.exports = pkg.exports;
20698
+ collectPackageEntryPaths(pkg.exports, extraPaths);
20699
+ }
20700
+ const fallbackEntries = ["./index.ts", "./index.js", "./src/index.ts", "./src/index.js"];
20701
+ if (Object.keys(entrypoints).length === 0) {
20702
+ for (const candidate of fallbackEntries) {
20703
+ if (existsSync24(resolve21(pluginDir, stripRelativePrefix(candidate)))) {
20704
+ entrypoints.entry = candidate;
20705
+ extraPaths.add(candidate);
20706
+ break;
20707
+ }
20708
+ }
20709
+ }
20710
+ const presentPaths = [...extraPaths].filter((entry) => existsSync24(resolve21(pluginDir, stripRelativePrefix(entry))));
20711
+ if (Object.keys(entrypoints).length === 0) {
20712
+ return { extraPaths: presentPaths };
20713
+ }
20714
+ return {
20715
+ extraPaths: presentPaths,
20716
+ platformOverrides: {
20717
+ opencode: {
20718
+ entrypoints
20719
+ }
20720
+ }
20721
+ };
20722
+ }
20723
+ function tryReadJson(path) {
20724
+ try {
20725
+ const parsed = JSON.parse(readFileSync14(path, "utf-8"));
20726
+ return asRecord4(parsed) ?? null;
20727
+ } catch {
20728
+ return null;
20729
+ }
20730
+ }
20731
+ function parseJsonMcpFile(path, platform) {
20732
+ const raw = tryReadJson(path);
20733
+ if (!raw) {
20734
+ return { servers: {}, runtimeAuthMode: "inline", warnings: [] };
20735
+ }
20736
+ const servers = extractJsonMcpServers(raw, platform, path);
20737
+ if (!servers) {
20738
+ return { servers: {}, runtimeAuthMode: "inline", warnings: [] };
20739
+ }
20740
+ return parseMcpServerRecords(servers, platform);
20741
+ }
20742
+ function extractJsonMcpServers(raw, platform, path) {
20743
+ if (platform === "opencode") {
20744
+ const mcp = asRecord4(raw.mcp);
20745
+ if (mcp) return mcp;
20746
+ }
20747
+ const mcpServers = asRecord4(raw.mcpServers);
20748
+ if (mcpServers) return mcpServers;
20749
+ if (typeof raw.mcpServers === "string") return null;
20750
+ if (path.endsWith("mcp.json") || path.endsWith(".mcp.json")) {
20751
+ return raw;
20752
+ }
20753
+ return null;
20754
+ }
20755
+ function parseTomlMcpFile(path, platform) {
20756
+ const content = readFileSync14(path, "utf-8");
20757
+ const servers = {};
20758
+ let currentServer;
20759
+ let currentSubtable;
20760
+ for (const rawLine of content.split(/\r?\n/)) {
20761
+ const line = stripTomlComment(rawLine).trim();
20762
+ if (!line) continue;
20763
+ const section = line.match(/^\[mcp_servers\.([A-Za-z0-9_.-]+)(?:\.([A-Za-z0-9_.-]+))?\]$/);
20764
+ if (section) {
20765
+ currentServer = section[1];
20766
+ currentSubtable = section[2];
20767
+ servers[currentServer] ??= {};
20768
+ if (currentSubtable) {
20769
+ const existing = servers[currentServer][currentSubtable];
20770
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
20771
+ servers[currentServer][currentSubtable] = {};
19384
20772
  }
19385
- result[name] = entry;
19386
20773
  }
19387
- return result;
19388
- } catch {
19389
- continue;
20774
+ continue;
20775
+ }
20776
+ if (!currentServer) continue;
20777
+ const assignment = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
20778
+ if (!assignment) continue;
20779
+ const key = assignment[1];
20780
+ const value = parseTomlValue(assignment[2].trim());
20781
+ if (currentSubtable) {
20782
+ const table = servers[currentServer][currentSubtable];
20783
+ table[key] = value;
20784
+ } else {
20785
+ servers[currentServer][key] = value;
20786
+ }
20787
+ }
20788
+ return parseMcpServerRecords(servers, platform);
20789
+ }
20790
+ function parseMcpServerRecords(servers, platform) {
20791
+ const parsed = {};
20792
+ const warnings = [];
20793
+ let runtimeAuthMode = "inline";
20794
+ const nativePlatformOverrides = buildNativeMcpPlatformOverrides(platform, servers);
20795
+ for (const [name, config] of Object.entries(servers)) {
20796
+ const cfg = asRecord4(config);
20797
+ if (!cfg) continue;
20798
+ const entryWarnings = [];
20799
+ const entry = parseMcpServerEntry(cfg, entryWarnings);
20800
+ if (entry.auth?.type === "platform" && entry.transport !== "stdio") {
20801
+ runtimeAuthMode = "platform";
20802
+ }
20803
+ if (entryWarnings.length > 0) {
20804
+ entry.warnings = entryWarnings;
20805
+ warnings.push(...entryWarnings.map((warning) => `${name}: ${warning}`));
20806
+ }
20807
+ parsed[name] = entry;
20808
+ }
20809
+ return {
20810
+ servers: parsed,
20811
+ runtimeAuthMode,
20812
+ warnings,
20813
+ ...nativePlatformOverrides ? { platformOverrides: nativePlatformOverrides } : {}
20814
+ };
20815
+ }
20816
+ function parseMcpServerEntry(cfg, warnings) {
20817
+ const entry = {};
20818
+ if (typeof cfg.url === "string") entry.url = cfg.url;
20819
+ if (cfg.type === "stdio" || cfg.command) {
20820
+ entry.transport = "stdio";
20821
+ entry.command = normalizeCommandValue(cfg.command);
20822
+ entry.args = normalizeStringArray(cfg.args);
20823
+ entry.env = normalizeEnvRecord(cfg.env);
20824
+ } else if (cfg.type === "sse") {
20825
+ entry.transport = "sse";
20826
+ } else {
20827
+ entry.transport = "http";
20828
+ }
20829
+ const auth = inferMigrateAuth(cfg, warnings);
20830
+ if (auth) entry.auth = auth;
20831
+ return entry;
20832
+ }
20833
+ function inferMigrateAuth(cfg, warnings) {
20834
+ const explicitAuth = asRecord4(cfg.auth);
20835
+ const explicitType = typeof explicitAuth?.type === "string" ? explicitAuth.type : void 0;
20836
+ if (explicitType === "platform") {
20837
+ return {
20838
+ type: "platform",
20839
+ mode: explicitAuth?.mode === "oauth" ? "oauth" : "oauth"
20840
+ };
20841
+ }
20842
+ if (explicitType === "none") {
20843
+ return { type: "none" };
20844
+ }
20845
+ if (explicitType === "bearer") {
20846
+ const envVar = firstString2(explicitAuth?.envVar, explicitAuth?.env_var);
20847
+ if (envVar) {
20848
+ return {
20849
+ type: "bearer",
20850
+ envVar,
20851
+ headerName: firstString2(explicitAuth?.headerName, explicitAuth?.header_name) ?? "Authorization",
20852
+ headerTemplate: firstString2(explicitAuth?.headerTemplate, explicitAuth?.header_template) ?? "Bearer ${value}"
20853
+ };
20854
+ }
20855
+ }
20856
+ if (explicitType === "header") {
20857
+ const envVar = firstString2(explicitAuth?.envVar, explicitAuth?.env_var);
20858
+ const headerName = firstString2(explicitAuth?.headerName, explicitAuth?.header_name);
20859
+ if (envVar && headerName) {
20860
+ return {
20861
+ type: "header",
20862
+ envVar,
20863
+ headerName,
20864
+ headerTemplate: firstString2(explicitAuth?.headerTemplate, explicitAuth?.header_template) ?? "${value}"
20865
+ };
20866
+ }
20867
+ }
20868
+ const bearerTokenEnv = firstString2(cfg.bearer_token_env_var, cfg.bearerTokenEnvVar);
20869
+ if (bearerTokenEnv) {
20870
+ return {
20871
+ type: "bearer",
20872
+ envVar: bearerTokenEnv,
20873
+ headerName: "Authorization",
20874
+ headerTemplate: "Bearer ${value}"
20875
+ };
20876
+ }
20877
+ const envHttpHeaders = asRecord4(cfg.env_http_headers ?? cfg.envHttpHeaders);
20878
+ if (envHttpHeaders) {
20879
+ const stringHeaders = Object.entries(envHttpHeaders).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, String(value)]);
20880
+ if (stringHeaders.length > 0) {
20881
+ const preferred = stringHeaders.find(([key]) => key.toLowerCase() === "authorization") ?? stringHeaders[0];
20882
+ const [headerName, envVar] = preferred;
20883
+ const extras = stringHeaders.filter(([key]) => key !== headerName);
20884
+ if (extras.length > 0) {
20885
+ warnings.push(`Preserved ${headerName} as canonical auth but native env_http_headers declared additional header auth keys (${extras.map(([key]) => key).join(", ")}). Review the migrated MCP config and platform overrides.`);
20886
+ }
20887
+ return {
20888
+ type: headerName.toLowerCase() === "authorization" ? "bearer" : "header",
20889
+ envVar,
20890
+ headerName,
20891
+ headerTemplate: headerName.toLowerCase() === "authorization" ? "Bearer ${value}" : "${value}"
20892
+ };
19390
20893
  }
19391
20894
  }
19392
- return {};
19393
- }
19394
- var HOOK_EVENT_MAP = {
19395
- SessionStart: "sessionStart",
19396
- SessionEnd: "sessionEnd",
19397
- PreToolUse: "preToolUse",
19398
- PostToolUse: "postToolUse",
19399
- BeforeShellExecution: "beforeShellExecution",
19400
- AfterShellExecution: "afterShellExecution",
19401
- BeforeMCPExecution: "beforeMCPExecution",
19402
- AfterMCPExecution: "afterMCPExecution",
19403
- AfterFileEdit: "afterFileEdit",
19404
- BeforeReadFile: "beforeReadFile",
19405
- BeforeSubmitPrompt: "beforeSubmitPrompt",
19406
- Stop: "stop",
19407
- // Also handle already-normalized names
19408
- sessionStart: "sessionStart",
19409
- sessionEnd: "sessionEnd",
19410
- preToolUse: "preToolUse",
19411
- postToolUse: "postToolUse"
19412
- };
19413
- function parseHooks(pluginDir, detection) {
19414
- const hooksPaths = [
19415
- resolve21(pluginDir, ".codex", "hooks.json"),
19416
- resolve21(pluginDir, "hooks.json"),
19417
- resolve21(pluginDir, "hooks", "hooks.json")
19418
- ];
19419
- try {
19420
- const manifest = JSON.parse(readFileSync14(detection.manifestPath, "utf-8"));
19421
- if (manifest.hooks && typeof manifest.hooks === "string") {
19422
- hooksPaths.unshift(resolve21(pluginDir, manifest.hooks));
20895
+ const headers = asRecord4(cfg.headers ?? cfg.http_headers ?? cfg.httpHeaders);
20896
+ if (!headers) return void 0;
20897
+ const envBackedHeaders = Object.entries(headers).filter(([, value]) => typeof value === "string").map(([headerName, rawValue]) => ({
20898
+ headerName,
20899
+ rawValue,
20900
+ envVar: extractEnvVar2(rawValue)
20901
+ })).filter((entry) => Boolean(entry.envVar));
20902
+ if (envBackedHeaders.length > 0) {
20903
+ const preferred = envBackedHeaders.find((entry) => entry.headerName.toLowerCase() === "authorization") ?? envBackedHeaders[0];
20904
+ const extras = envBackedHeaders.filter((entry) => entry.headerName !== preferred.headerName);
20905
+ if (extras.length > 0) {
20906
+ warnings.push(`Preserved ${preferred.headerName} as canonical auth but native headers declared additional env-backed auth keys (${extras.map((entry) => entry.headerName).join(", ")}). Review the migrated MCP config and platform overrides.`);
19423
20907
  }
19424
- } catch {
20908
+ return {
20909
+ type: preferred.headerName.toLowerCase() === "authorization" ? "bearer" : "header",
20910
+ envVar: preferred.envVar,
20911
+ headerName: preferred.headerName,
20912
+ headerTemplate: preferred.rawValue.replace(envReferencePattern(preferred.envVar), "${value}")
20913
+ };
19425
20914
  }
19426
- for (const hooksPath of hooksPaths) {
19427
- if (!existsSync24(hooksPath)) continue;
20915
+ return void 0;
20916
+ }
20917
+ function normalizeCommandValue(value) {
20918
+ if (typeof value === "string") return value;
20919
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
20920
+ return void 0;
20921
+ }
20922
+ function normalizeStringArray(value) {
20923
+ if (!Array.isArray(value)) return void 0;
20924
+ return value.map(String);
20925
+ }
20926
+ function normalizeEnvRecord(value) {
20927
+ const record = asRecord4(value);
20928
+ if (!record) return void 0;
20929
+ const env = Object.fromEntries(
20930
+ Object.entries(record).filter(([, rawValue]) => typeof rawValue === "string").map(([key, rawValue]) => [key, rawValue])
20931
+ );
20932
+ return Object.keys(env).length > 0 ? env : void 0;
20933
+ }
20934
+ function asRecord4(value) {
20935
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
20936
+ return value;
20937
+ }
20938
+ function firstString2(...values) {
20939
+ for (const value of values) {
20940
+ if (typeof value === "string" && value.trim()) return value.trim();
20941
+ }
20942
+ return void 0;
20943
+ }
20944
+ function extractEnvVar2(value) {
20945
+ const match = value.match(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/);
20946
+ return match?.[1] ?? match?.[2];
20947
+ }
20948
+ function envReferencePattern(envVar) {
20949
+ return new RegExp(`\\$\\{(?:env:)?${escapeRegExp2(envVar)}\\}|\\$${escapeRegExp2(envVar)}`);
20950
+ }
20951
+ function escapeRegExp2(value) {
20952
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20953
+ }
20954
+ function parseTopLevelStringFrontmatter(frontmatterLines, key) {
20955
+ const match = frontmatterLines.find((line) => new RegExp(`^${escapeRegExp2(key)}\\s*:`).test(line.trim()));
20956
+ if (!match) return void 0;
20957
+ const rawValue = match.replace(/^[^:]+:\s*/, "");
20958
+ return unquoteFrontmatterValue(rawValue);
20959
+ }
20960
+ function parseTopLevelBooleanFrontmatter(frontmatterLines, key) {
20961
+ const value = parseTopLevelStringFrontmatter(frontmatterLines, key)?.toLowerCase();
20962
+ if (value === "true") return true;
20963
+ if (value === "false") return false;
20964
+ return void 0;
20965
+ }
20966
+ function parseTopLevelStringOrStringArrayFrontmatter(frontmatterLines, key) {
20967
+ const match = frontmatterLines.find((line) => new RegExp(`^${escapeRegExp2(key)}\\s*:`).test(line.trim()));
20968
+ if (!match) return void 0;
20969
+ const rawValue = match.replace(/^[^:]+:\s*/, "").trim();
20970
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
19428
20971
  try {
19429
- const raw = JSON.parse(readFileSync14(hooksPath, "utf-8"));
19430
- const hooksObj = raw.hooks ?? raw;
19431
- if (!hooksObj || typeof hooksObj !== "object") continue;
19432
- const result = {};
19433
- for (const [event, entries] of Object.entries(hooksObj)) {
19434
- const normalizedEvent = HOOK_EVENT_MAP[event] ?? event;
19435
- const hookEntries = [];
19436
- if (!Array.isArray(entries)) continue;
19437
- for (const entry of entries) {
19438
- if (entry.hooks && Array.isArray(entry.hooks)) {
19439
- for (const hook of entry.hooks) {
19440
- if (hook.command) {
19441
- hookEntries.push({
19442
- command: hook.command,
19443
- ...hook.timeout && { timeout: hook.timeout }
19444
- });
19445
- }
19446
- }
19447
- } else if (entry.command) {
19448
- hookEntries.push({
19449
- command: entry.command,
19450
- ...entry.timeout && { timeout: entry.timeout },
19451
- ...entry.matcher && { matcher: entry.matcher }
19452
- });
19453
- }
19454
- }
19455
- if (hookEntries.length > 0) {
19456
- result[normalizedEvent] = hookEntries;
19457
- }
20972
+ const parsed = JSON.parse(rawValue);
20973
+ if (Array.isArray(parsed) && parsed.every((value) => typeof value === "string")) {
20974
+ return parsed;
19458
20975
  }
19459
- return result;
19460
20976
  } catch {
19461
- continue;
20977
+ return void 0;
19462
20978
  }
19463
20979
  }
19464
- return {};
20980
+ return unquoteFrontmatterValue(rawValue);
19465
20981
  }
19466
- function findInstructions(pluginDir) {
19467
- const candidates = [
19468
- "CLAUDE.md",
19469
- "AGENTS.md",
19470
- "instructions.md",
19471
- "INSTRUCTIONS.md",
19472
- "README.md"
19473
- ];
19474
- for (const candidate of candidates) {
19475
- const filePath = resolve21(pluginDir, candidate);
19476
- if (existsSync24(filePath)) {
19477
- return `./${candidate}`;
19478
- }
20982
+ function unquoteFrontmatterValue(value) {
20983
+ const trimmed = value.trim();
20984
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
20985
+ return trimmed.slice(1, -1);
19479
20986
  }
19480
- return void 0;
20987
+ return trimmed;
19481
20988
  }
19482
20989
  var CANONICAL_SOURCE_CANDIDATES = {
19483
20990
  skills: ["skills", ".claude/skills", ".cursor/skills", ".agents/skills", ".opencode/skills"],
@@ -19559,7 +21066,12 @@ function inferPermissionsFromMigratedSkills(pluginDir, sourcePaths) {
19559
21066
  if (!entry.isDirectory()) continue;
19560
21067
  const skillPath = resolve21(skillsDir, entry.name, "SKILL.md");
19561
21068
  if (!existsSync24(skillPath)) continue;
19562
- const skill = parseSkillMarkdown(readFileSync14(skillPath, "utf-8"));
21069
+ const skill = getCanonicalSkillMetadata({
21070
+ filePath: skillPath,
21071
+ relativeDir: entry.name,
21072
+ dirName: entry.name,
21073
+ ...parseSkillMarkdown(readFileSync14(skillPath, "utf-8"))
21074
+ });
19563
21075
  const inferredRules = skill.allowedTools.map(normalizeMigratedAllowedTool).filter((rule) => Boolean(rule));
19564
21076
  if (inferredRules.length === 0) continue;
19565
21077
  sawAllowedTools = true;
@@ -19581,7 +21093,7 @@ function inferPermissionsFromMigratedSkills(pluginDir, sourcePaths) {
19581
21093
  if (skill.shell) sourceFrontmatter.shell = skill.shell;
19582
21094
  skillPolicies.push({
19583
21095
  skillDir: entry.name,
19584
- title: skill.name ?? skill.firstHeading ?? titleCaseFromDirName(entry.name),
21096
+ title: skill.title,
19585
21097
  description: skill.description,
19586
21098
  ...Object.keys(sourceFrontmatter).length > 0 ? { sourceFrontmatter } : {},
19587
21099
  source: {
@@ -19612,10 +21124,11 @@ function readMigratedSkills(pluginDir, sourcePaths) {
19612
21124
  if (sourcePaths.skills) {
19613
21125
  const skillsDir = resolve21(pluginDir, stripRelativePrefix(sourcePaths.skills));
19614
21126
  for (const skill of readCanonicalSkillFiles(skillsDir)) {
21127
+ const metadata = getCanonicalSkillMetadata(skill);
19615
21128
  skills.push({
19616
21129
  dirName: skill.dirName,
19617
- title: skill.name ?? skill.firstHeading ?? titleCaseFromDirName(skill.dirName),
19618
- description: skill.description,
21130
+ title: metadata.title,
21131
+ description: metadata.description,
19619
21132
  toolNames: []
19620
21133
  });
19621
21134
  }
@@ -19674,9 +21187,84 @@ function parseCodexAgentToml(content) {
19674
21187
  description: readTomlScalarValue(content, "description"),
19675
21188
  model: readTomlScalarValue(content, "model"),
19676
21189
  effort: readTomlScalarValue(content, "model_reasoning_effort"),
21190
+ sandboxMode: readTomlScalarValue(content, "sandbox_mode"),
19677
21191
  developerInstructions: readTomlMultilineValue(content, "developer_instructions")
19678
21192
  };
19679
21193
  }
21194
+ function stripTomlComment(line) {
21195
+ let inString = false;
21196
+ let quote2 = "";
21197
+ for (let i = 0; i < line.length; i += 1) {
21198
+ const char = line[i];
21199
+ if ((char === '"' || char === "'") && line[i - 1] !== "\\") {
21200
+ if (!inString) {
21201
+ inString = true;
21202
+ quote2 = char;
21203
+ } else if (quote2 === char) {
21204
+ inString = false;
21205
+ quote2 = "";
21206
+ }
21207
+ continue;
21208
+ }
21209
+ if (char === "#" && !inString) return line.slice(0, i);
21210
+ }
21211
+ return line;
21212
+ }
21213
+ function parseTomlValue(value) {
21214
+ if (value.startsWith('"') && value.endsWith('"')) return unquoteTomlString(value);
21215
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
21216
+ if (value.startsWith("[") && value.endsWith("]")) {
21217
+ const inner = value.slice(1, -1).trim();
21218
+ if (!inner) return [];
21219
+ return splitTomlList(inner).map((part) => parseTomlValue(part.trim()));
21220
+ }
21221
+ if (value.startsWith("{") && value.endsWith("}")) {
21222
+ const inner = value.slice(1, -1).trim();
21223
+ const result = {};
21224
+ if (!inner) return result;
21225
+ for (const part of splitTomlList(inner)) {
21226
+ const assignment = part.trim().match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
21227
+ if (!assignment) continue;
21228
+ result[assignment[1]] = parseTomlValue(assignment[2].trim());
21229
+ }
21230
+ return result;
21231
+ }
21232
+ if (value === "true") return true;
21233
+ if (value === "false") return false;
21234
+ return value;
21235
+ }
21236
+ function splitTomlList(value) {
21237
+ const parts = [];
21238
+ let current = "";
21239
+ let inString = false;
21240
+ let quote2 = "";
21241
+ let braceDepth = 0;
21242
+ for (let i = 0; i < value.length; i += 1) {
21243
+ const char = value[i];
21244
+ if ((char === '"' || char === "'") && value[i - 1] !== "\\") {
21245
+ if (!inString) {
21246
+ inString = true;
21247
+ quote2 = char;
21248
+ } else if (quote2 === char) {
21249
+ inString = false;
21250
+ quote2 = "";
21251
+ }
21252
+ }
21253
+ if (!inString && char === "{") braceDepth += 1;
21254
+ if (!inString && char === "}") braceDepth -= 1;
21255
+ if (!inString && braceDepth === 0 && char === ",") {
21256
+ parts.push(current);
21257
+ current = "";
21258
+ continue;
21259
+ }
21260
+ current += char;
21261
+ }
21262
+ if (current.trim()) parts.push(current);
21263
+ return parts;
21264
+ }
21265
+ function unquoteTomlString(value) {
21266
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
21267
+ }
19680
21268
  function renderMigratedAgentMarkdown(fileStem, parsed) {
19681
21269
  const agentName = toKebabCase2(parsed.name ?? fileStem) || "agent";
19682
21270
  const title = parsed.name ?? titleCaseFromDirName(agentName);
@@ -19692,6 +21280,7 @@ function renderMigratedAgentMarkdown(fileStem, parsed) {
19692
21280
  ...parsed.description ? [`description: ${JSON.stringify(parsed.description)}`] : [],
19693
21281
  ...parsed.model ? [`model: ${JSON.stringify(parsed.model)}`] : [],
19694
21282
  ...parsed.effort ? [`model_reasoning_effort: ${JSON.stringify(parsed.effort)}`] : [],
21283
+ ...parsed.sandboxMode ? [`sandbox_mode: ${JSON.stringify(parsed.sandboxMode)}`] : [],
19695
21284
  "---",
19696
21285
  "",
19697
21286
  `# ${title}`,
@@ -19855,6 +21444,12 @@ function normalizeMigrateAuth(auth) {
19855
21444
  if (auth.type === "none") {
19856
21445
  return { type: "none" };
19857
21446
  }
21447
+ if (auth.type === "platform") {
21448
+ return {
21449
+ type: "platform",
21450
+ mode: auth.mode ?? "oauth"
21451
+ };
21452
+ }
19858
21453
  if (auth.type === "bearer" && auth.envVar) {
19859
21454
  return {
19860
21455
  type: "bearer",
@@ -19880,6 +21475,7 @@ function buildMigratedScaffoldMetadata(result, outputDir) {
19880
21475
  const generatedHookEvents = Object.keys(result.hooks);
19881
21476
  const managedFiles = [
19882
21477
  ...result.instructions ? [result.instructions.replace(/^\.\//, "")] : [],
21478
+ ...result.extraCopyPaths.map((path) => path.replace(/^\.\//, "")),
19883
21479
  ...["skills", "commands", "agents", "scripts", "assets"].flatMap((dir) => {
19884
21480
  if (!result.sourcePaths[dir]) return [];
19885
21481
  const baseDir = dir;
@@ -19926,7 +21522,7 @@ function buildMigratedScaffoldMetadata(result, outputDir) {
19926
21522
  requestedHookMode: generatedHookEvents.length > 0 ? "safe" : "none",
19927
21523
  generatedHookMode: generatedHookEvents.length > 0 ? "safe" : "none",
19928
21524
  generatedHookEvents,
19929
- runtimeAuthMode: "inline"
21525
+ runtimeAuthMode: result.runtimeAuthMode
19930
21526
  },
19931
21527
  userConfig: [],
19932
21528
  tools: [],
@@ -20007,6 +21603,9 @@ function generateConfigTs(result) {
20007
21603
  if (result.manifest.keywords && result.manifest.keywords.length > 0) {
20008
21604
  lines.push(` keywords: [${result.manifest.keywords.map((k) => quote(k)).join(", ")}],`);
20009
21605
  }
21606
+ if (result.manifest.brand && Object.keys(result.manifest.brand).length > 0) {
21607
+ lines.push(" brand: " + renderIndentedTsValue(result.manifest.brand, 2) + ",");
21608
+ }
20010
21609
  lines.push("");
20011
21610
  if (result.sourcePaths.skills) {
20012
21611
  lines.push(` skills: './skills/',`);
@@ -20053,6 +21652,9 @@ function generateConfigTs(result) {
20053
21652
  for (const name2 of mcpNames) {
20054
21653
  const server = result.mcp[name2];
20055
21654
  lines.push(` ${quote(name2)}: {`);
21655
+ for (const warning of server.warnings ?? []) {
21656
+ lines.push(` // ${warning}`);
21657
+ }
20056
21658
  if (server.url) lines.push(` url: ${quote(server.url)},`);
20057
21659
  if (server.transport && server.transport !== "http") {
20058
21660
  lines.push(` transport: ${quote(server.transport)},`);
@@ -20072,6 +21674,9 @@ function generateConfigTs(result) {
20072
21674
  lines.push(" auth: {");
20073
21675
  lines.push(` type: ${quote(server.auth.type)},`);
20074
21676
  if (server.auth.envVar) lines.push(` envVar: ${quote(server.auth.envVar)},`);
21677
+ if (server.auth.mode && server.auth.type === "platform") {
21678
+ lines.push(` mode: ${quote(server.auth.mode)},`);
21679
+ }
20075
21680
  if (server.auth.headerName && server.auth.headerName !== "Authorization") {
20076
21681
  lines.push(` headerName: ${quote(server.auth.headerName)},`);
20077
21682
  }
@@ -20084,6 +21689,11 @@ function generateConfigTs(result) {
20084
21689
  }
20085
21690
  lines.push(" },");
20086
21691
  }
21692
+ const platforms = buildMigratedPlatformOverrides(result);
21693
+ if (platforms && Object.keys(platforms).length > 0) {
21694
+ lines.push("");
21695
+ lines.push(" platforms: " + renderIndentedTsValue(platforms, 2) + ",");
21696
+ }
20087
21697
  const hookEvents = Object.keys(result.hooks);
20088
21698
  if (hookEvents.length > 0) {
20089
21699
  lines.push("");
@@ -20092,9 +21702,25 @@ function generateConfigTs(result) {
20092
21702
  const entries = result.hooks[event];
20093
21703
  lines.push(` ${event}: [`);
20094
21704
  for (const entry of entries) {
20095
- const parts = [`command: ${quote(entry.command)}`];
20096
- if (entry.timeout) parts.push(`timeout: ${entry.timeout}`);
20097
- if (entry.matcher) parts.push(`matcher: ${quote(entry.matcher)}`);
21705
+ const parts = [];
21706
+ if (entry.type && entry.type !== "command") parts.push(`type: ${renderTsValue(entry.type)}`);
21707
+ if (entry.command) parts.push(`command: ${renderTsValue(entry.command)}`);
21708
+ if (entry.prompt) parts.push(`prompt: ${renderTsValue(entry.prompt)}`);
21709
+ if (entry.model) parts.push(`model: ${renderTsValue(entry.model)}`);
21710
+ if (entry.url) parts.push(`url: ${renderTsValue(entry.url)}`);
21711
+ if (entry.headers) parts.push(`headers: ${renderTsValue(entry.headers)}`);
21712
+ if (entry.allowedEnvVars) parts.push(`allowedEnvVars: ${renderTsValue(entry.allowedEnvVars)}`);
21713
+ if (entry.server) parts.push(`server: ${renderTsValue(entry.server)}`);
21714
+ if (entry.tool) parts.push(`tool: ${renderTsValue(entry.tool)}`);
21715
+ if (entry.input) parts.push(`input: ${renderTsValue(entry.input)}`);
21716
+ if (entry.if) parts.push(`if: ${renderTsValue(entry.if)}`);
21717
+ if (entry.async !== void 0) parts.push(`async: ${renderTsValue(entry.async)}`);
21718
+ if (entry.asyncRewake !== void 0) parts.push(`asyncRewake: ${renderTsValue(entry.asyncRewake)}`);
21719
+ if (entry.shell) parts.push(`shell: ${renderTsValue(entry.shell)}`);
21720
+ if (entry.timeout !== void 0) parts.push(`timeout: ${renderTsValue(entry.timeout)}`);
21721
+ if (entry.matcher !== void 0) parts.push(`matcher: ${renderTsValue(entry.matcher)}`);
21722
+ if (entry.failClosed !== void 0) parts.push(`failClosed: ${renderTsValue(entry.failClosed)}`);
21723
+ if (entry.loop_limit !== void 0) parts.push(`loop_limit: ${renderTsValue(entry.loop_limit)}`);
20098
21724
  lines.push(` { ${parts.join(", ")} },`);
20099
21725
  }
20100
21726
  lines.push(" ],");
@@ -20111,6 +21737,65 @@ function generateConfigTs(result) {
20111
21737
  function quote(s) {
20112
21738
  return `'${s.replace(/'/g, "\\'")}'`;
20113
21739
  }
21740
+ function renderTsValue(value) {
21741
+ if (typeof value === "string") return quote(value);
21742
+ return JSON.stringify(value);
21743
+ }
21744
+ function renderIndentedTsValue(value, indentLevel) {
21745
+ const rendered = JSON.stringify(value, null, 2);
21746
+ if (!rendered.includes("\n")) return rendered;
21747
+ const indent = " ".repeat(indentLevel);
21748
+ return rendered.split("\n").map((line, index) => index === 0 ? line : indent + line).join("\n");
21749
+ }
21750
+ function buildMigratedPlatformOverrides(result) {
21751
+ const platforms = cloneRecordMap(result.manifest.platforms);
21752
+ if (result.runtimeAuthMode === "platform") {
21753
+ platforms["claude-code"] = {
21754
+ ...platforms["claude-code"] ?? {},
21755
+ mcpAuth: "platform"
21756
+ };
21757
+ platforms.cursor = {
21758
+ ...platforms.cursor ?? {},
21759
+ mcpAuth: "platform"
21760
+ };
21761
+ }
21762
+ return mergePlatformOverrideMaps(platforms);
21763
+ }
21764
+ function cloneRecordMap(input) {
21765
+ if (!input) return {};
21766
+ return JSON.parse(JSON.stringify(input));
21767
+ }
21768
+ function mergePlatformOverrideMaps(...maps) {
21769
+ const merged = {};
21770
+ for (const map of maps) {
21771
+ if (!map) continue;
21772
+ for (const [platform, override] of Object.entries(map)) {
21773
+ const current = asRecord4(merged[platform]) ?? {};
21774
+ const next = asRecord4(override) ?? {};
21775
+ const mergedPlatform = {
21776
+ ...current,
21777
+ ...next
21778
+ };
21779
+ const currentMcpServers = asRecord4(current.mcpServers);
21780
+ const nextMcpServers = asRecord4(next.mcpServers);
21781
+ if (currentMcpServers || nextMcpServers) {
21782
+ const mergedMcpServers = {};
21783
+ for (const [serverName, serverConfig] of Object.entries(currentMcpServers ?? {})) {
21784
+ const currentServerConfig = asRecord4(serverConfig);
21785
+ mergedMcpServers[serverName] = currentServerConfig ? { ...currentServerConfig } : serverConfig;
21786
+ }
21787
+ for (const [serverName, serverConfig] of Object.entries(nextMcpServers ?? {})) {
21788
+ const previousServerConfig = asRecord4(mergedMcpServers[serverName]);
21789
+ const nextServerConfig = asRecord4(serverConfig);
21790
+ mergedMcpServers[serverName] = previousServerConfig && nextServerConfig ? { ...previousServerConfig, ...nextServerConfig } : serverConfig;
21791
+ }
21792
+ mergedPlatform.mcpServers = mergedMcpServers;
21793
+ }
21794
+ merged[platform] = mergedPlatform;
21795
+ }
21796
+ }
21797
+ return Object.keys(merged).length > 0 ? merged : void 0;
21798
+ }
20114
21799
  async function migrate(inputPath) {
20115
21800
  const pluginDir = resolve21(inputPath);
20116
21801
  const outputDir = process.cwd();
@@ -20124,6 +21809,7 @@ async function migrate(inputPath) {
20124
21809
  console.error("Error: Could not detect plugin platform.");
20125
21810
  console.error("Expected one of:");
20126
21811
  console.error(" .claude-plugin/plugin.json");
21812
+ console.error(" or a manifest-less Claude plugin with CLAUDE.md");
20127
21813
  console.error(" .cursor-plugin/plugin.json");
20128
21814
  console.error(" .codex-plugin/plugin.json");
20129
21815
  console.error(" package.json with @opencode-ai/plugin dependency");
@@ -20133,7 +21819,8 @@ async function migrate(inputPath) {
20133
21819
  const manifest = parseManifest(detection);
20134
21820
  console.log(` name: ${manifest.name ?? "(none)"}`);
20135
21821
  console.log(` version: ${manifest.version ?? "(none)"}`);
20136
- const mcp = parseMcp(pluginDir, detection);
21822
+ const parsedMcp = parseMcp(pluginDir, detection);
21823
+ const mcp = parsedMcp.servers;
20137
21824
  const mcpCount = Object.keys(mcp).length;
20138
21825
  if (mcpCount > 0) {
20139
21826
  console.log(` mcp servers: ${Object.keys(mcp).join(", ")}`);
@@ -20143,10 +21830,32 @@ async function migrate(inputPath) {
20143
21830
  if (hookCount > 0) {
20144
21831
  console.log(` hooks: ${Object.keys(hooks).join(", ")}`);
20145
21832
  }
20146
- const instructions = findInstructions(pluginDir);
21833
+ const instructionSources = findInstructionSources(pluginDir, detection);
21834
+ const instructions = instructionSources.primary;
20147
21835
  if (instructions) {
20148
21836
  console.log(` instructions: ${instructions}`);
20149
21837
  }
21838
+ if (instructionSources.platformOverrides) {
21839
+ manifest.platforms = mergePlatformOverrideMaps(manifest.platforms, instructionSources.platformOverrides);
21840
+ }
21841
+ const cursorRules = parseCursorRules(pluginDir, detection);
21842
+ const openCodeDistributionSources = findOpenCodeDistributionSources(pluginDir, detection);
21843
+ if (cursorRules.length > 0) {
21844
+ manifest.platforms = {
21845
+ ...manifest.platforms ?? {},
21846
+ cursor: {
21847
+ ...manifest.platforms?.cursor ?? {},
21848
+ rules: cursorRules
21849
+ }
21850
+ };
21851
+ console.log(` cursor rules: ${cursorRules.length}`);
21852
+ }
21853
+ if (openCodeDistributionSources.platformOverrides) {
21854
+ manifest.platforms = mergePlatformOverrideMaps(manifest.platforms, openCodeDistributionSources.platformOverrides);
21855
+ }
21856
+ if (parsedMcp.platformOverrides) {
21857
+ manifest.platforms = mergePlatformOverrideMaps(manifest.platforms, parsedMcp.platformOverrides);
21858
+ }
20150
21859
  const sourcePaths = detectCanonicalSourcePaths(pluginDir);
20151
21860
  const passthrough = detectPassthroughDirs(pluginDir, mcp);
20152
21861
  const persistedSkills = readMigratedSkills(pluginDir, sourcePaths);
@@ -20165,6 +21874,7 @@ async function migrate(inputPath) {
20165
21874
  platform: detection.platform,
20166
21875
  manifest,
20167
21876
  mcp,
21877
+ runtimeAuthMode: parsedMcp.runtimeAuthMode,
20168
21878
  hooks,
20169
21879
  permissions: inferredPermissions.permissions,
20170
21880
  permissionNotes: inferredPermissions.notes,
@@ -20172,6 +21882,11 @@ async function migrate(inputPath) {
20172
21882
  instructions,
20173
21883
  sourcePaths,
20174
21884
  persistedSkills,
21885
+ warnings: parsedMcp.warnings,
21886
+ extraCopyPaths: [.../* @__PURE__ */ new Set([
21887
+ ...instructionSources.extraPaths,
21888
+ ...openCodeDistributionSources.extraPaths
21889
+ ])].sort(),
20175
21890
  ...inferredPermissions.skillPolicies.length > 0 ? {
20176
21891
  compilerIntent: {
20177
21892
  version: 1,
@@ -20204,6 +21919,14 @@ Generated pluxx.config.ts`);
20204
21919
  console.log(`Copied: ${instructions}`);
20205
21920
  }
20206
21921
  }
21922
+ for (const copiedPath of result.extraCopyPaths) {
21923
+ const srcPath = resolve21(pluginDir, copiedPath);
21924
+ const destPath = resolve21(outputDir, copiedPath);
21925
+ if (existsSync24(destPath)) continue;
21926
+ const content = readFileSync14(srcPath, "utf-8");
21927
+ await writeTextFile(destPath, content);
21928
+ console.log(`Copied: ${copiedPath}`);
21929
+ }
20207
21930
  const taxonomyPath = resolve21(outputDir, MCP_TAXONOMY_PATH);
20208
21931
  const metadataPath = resolve21(outputDir, MCP_SCAFFOLD_METADATA_PATH);
20209
21932
  mkdirSync5(resolve21(outputDir, ".pluxx"), { recursive: true });
@@ -20234,7 +21957,7 @@ Generated pluxx.config.ts`);
20234
21957
 
20235
21958
  // src/cli/mcp-proxy.ts
20236
21959
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync15 } from "fs";
20237
- import { dirname as dirname8, resolve as resolve22 } from "path";
21960
+ import { dirname as dirname9, resolve as resolve22 } from "path";
20238
21961
  import * as readline3 from "readline";
20239
21962
  function usage() {
20240
21963
  return [
@@ -20311,7 +22034,7 @@ async function loadReplayTape(filepath) {
20311
22034
  }
20312
22035
  async function writeTape(filepath, tape) {
20313
22036
  const absolutePath = resolve22(process.cwd(), filepath);
20314
- mkdirSync6(dirname8(absolutePath), { recursive: true });
22037
+ mkdirSync6(dirname9(absolutePath), { recursive: true });
20315
22038
  await writeTextFile(absolutePath, `${JSON.stringify(tape, null, 2)}
20316
22039
  `);
20317
22040
  }
@@ -21488,17 +23211,17 @@ ${c2}
21488
23211
  import { basename as basename8, resolve as resolve27 } from "path";
21489
23212
  import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
21490
23213
  import { tmpdir as tmpdir5 } from "os";
21491
- import { spawn as spawn5, spawnSync as spawnSync3 } from "child_process";
23214
+ import { spawn as spawn5, spawnSync as spawnSync4 } from "child_process";
21492
23215
 
21493
23216
  // src/cli/publish.ts
21494
23217
  import { chmodSync, existsSync as existsSync25, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
21495
23218
  import { createHash } from "crypto";
21496
23219
  import { resolve as resolve23 } from "path";
21497
- import { spawnSync as spawnSync2 } from "child_process";
23220
+ import { spawnSync as spawnSync3 } from "child_process";
21498
23221
  import { tmpdir as tmpdir3 } from "os";
21499
23222
  var INSTALLER_TARGETS = ["claude-code", "cursor", "codex", "opencode"];
21500
23223
  function runCommandDefault2(command2, args2, options) {
21501
- const result = spawnSync2(command2, args2, {
23224
+ const result = spawnSync3(command2, args2, {
21502
23225
  cwd: options?.cwd,
21503
23226
  encoding: "utf-8"
21504
23227
  });
@@ -22873,7 +24596,14 @@ async function runBehavioralSuite(rootDir, config, targets, options = {}) {
22873
24596
  for (const platform of selectedPlatforms) {
22874
24597
  const targetConfig = behavioralCase.targets[platform];
22875
24598
  if (!targetConfig) continue;
22876
- checks.push(await runBehavioralCheck(rootDir, config, behavioralCase.name, platform, targetConfig));
24599
+ checks.push(await runBehavioralCheck(
24600
+ rootDir,
24601
+ config,
24602
+ behavioralCase.name,
24603
+ behavioralCase.commandId,
24604
+ platform,
24605
+ targetConfig
24606
+ ));
22877
24607
  }
22878
24608
  }
22879
24609
  return {
@@ -22903,17 +24633,31 @@ function loadBehavioralCases(rootDir, targets, promptOverride) {
22903
24633
  }
22904
24634
  return parsed.cases;
22905
24635
  }
22906
- async function runBehavioralCheck(rootDir, config, caseName, platform, targetConfig) {
24636
+ async function runBehavioralCheck(rootDir, config, caseName, caseCommandId, platform, targetConfig) {
22907
24637
  const prompt = targetConfig.prompt.trim();
22908
24638
  if (!prompt) {
22909
24639
  throw new Error(`Behavioral smoke case "${caseName}" for ${platform} is missing a prompt.`);
22910
24640
  }
22911
- const command2 = await buildBehavioralCommand(platform, prompt, rootDir);
24641
+ const commandId = targetConfig.commandId ?? caseCommandId;
24642
+ if (commandId) {
24643
+ if (!behavioralPromptReferencesCommand(prompt, commandId)) {
24644
+ throw new Error(
24645
+ `Behavioral smoke case "${caseName}" for ${platform} declares commandId "${commandId}" but the prompt does not reference that command explicitly.`
24646
+ );
24647
+ }
24648
+ if (!targetConfig.require?.length) {
24649
+ throw new Error(
24650
+ `Behavioral smoke case "${caseName}" for ${platform} declares commandId "${commandId}" but does not define any required output markers.`
24651
+ );
24652
+ }
24653
+ }
24654
+ const expectedExitCodes = targetConfig.expectedExitCodes?.length ? [...new Set(targetConfig.expectedExitCodes)] : targetConfig.expectFailure ? [1] : [0];
24655
+ const command2 = await buildBehavioralCommand(platform, prompt, rootDir, targetConfig);
22912
24656
  const execution = await executeBehavioralCommand(platform, command2, rootDir);
22913
24657
  const responseText = execution.response.trim();
22914
24658
  const failures = [];
22915
- if (execution.exitCode !== 0) {
22916
- failures.push(`runner exited with code ${execution.exitCode}`);
24659
+ if (!expectedExitCodes.includes(execution.exitCode)) {
24660
+ failures.push(`runner exited with code ${execution.exitCode}; expected one of ${expectedExitCodes.join(", ")}`);
22917
24661
  }
22918
24662
  if (!responseText) {
22919
24663
  failures.push("runner returned no response text");
@@ -22932,6 +24676,7 @@ async function runBehavioralCheck(rootDir, config, caseName, platform, targetCon
22932
24676
  caseName,
22933
24677
  platform,
22934
24678
  prompt,
24679
+ commandId,
22935
24680
  command: command2,
22936
24681
  ok: failures.length === 0,
22937
24682
  exitCode: execution.exitCode,
@@ -22939,10 +24684,12 @@ async function runBehavioralCheck(rootDir, config, caseName, platform, targetCon
22939
24684
  responsePreview: truncate2(responseText, 220),
22940
24685
  require: targetConfig.require,
22941
24686
  forbid: targetConfig.forbid,
24687
+ expectedExitCodes,
22942
24688
  failures
22943
24689
  };
22944
24690
  }
22945
- async function buildBehavioralCommand(platform, prompt, workspace) {
24691
+ async function buildBehavioralCommand(platform, prompt, workspace, targetConfig) {
24692
+ const runnerArgs = targetConfig.runnerArgs ?? [];
22946
24693
  if (platform === "claude-code") {
22947
24694
  return [
22948
24695
  "claude",
@@ -22951,6 +24698,7 @@ async function buildBehavioralCommand(platform, prompt, workspace) {
22951
24698
  "text",
22952
24699
  "--permission-mode",
22953
24700
  "acceptEdits",
24701
+ ...runnerArgs,
22954
24702
  "-p",
22955
24703
  prompt
22956
24704
  ];
@@ -22968,6 +24716,7 @@ async function buildBehavioralCommand(platform, prompt, workspace) {
22968
24716
  "--workspace",
22969
24717
  workspace,
22970
24718
  "--force",
24719
+ ...runnerArgs,
22971
24720
  prompt
22972
24721
  ];
22973
24722
  }
@@ -22978,10 +24727,11 @@ async function buildBehavioralCommand(platform, prompt, workspace) {
22978
24727
  "--ephemeral",
22979
24728
  "--skip-git-repo-check",
22980
24729
  "--full-auto",
24730
+ ...runnerArgs,
22981
24731
  prompt
22982
24732
  ];
22983
24733
  }
22984
- return ["opencode", "run", prompt];
24734
+ return ["opencode", "run", ...runnerArgs, prompt];
22985
24735
  }
22986
24736
  async function executeBehavioralCommand(platform, command2, cwd) {
22987
24737
  let codexOutputDir = null;
@@ -23064,6 +24814,12 @@ function truncate2(value, length) {
23064
24814
  function includesNeedle(haystack, needle) {
23065
24815
  return haystack.toLowerCase().includes(needle.trim().toLowerCase());
23066
24816
  }
24817
+ function behavioralPromptReferencesCommand(prompt, commandId) {
24818
+ const normalizedPrompt = prompt.toLowerCase();
24819
+ const normalizedCommandId = commandId.trim().toLowerCase();
24820
+ if (!normalizedCommandId) return false;
24821
+ return normalizedPrompt.includes(`/${normalizedCommandId}`) || normalizedPrompt.includes(`:${normalizedCommandId}`) || normalizedPrompt.includes(`command ${normalizedCommandId}`) || normalizedPrompt.includes(`command \`${normalizedCommandId}\``) || normalizedPrompt.includes(normalizedCommandId);
24822
+ }
23067
24823
  function shellQuote2(value) {
23068
24824
  if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
23069
24825
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -23082,7 +24838,7 @@ function discoverInstalledMcpServers(options = {}) {
23082
24838
  const seen = /* @__PURE__ */ new Set();
23083
24839
  for (const candidate of installedMcpFileCandidates(rootDir, homeDir)) {
23084
24840
  if (!hostSet.has(candidate.host) || !existsSync28(candidate.path)) continue;
23085
- const parsed = candidate.parser === "json" ? parseJsonMcpFile(candidate.path, candidate.host) : parseCodexTomlMcpFile(candidate.path);
24841
+ const parsed = candidate.parser === "json" ? parseJsonMcpFile2(candidate.path, candidate.host) : parseCodexTomlMcpFile(candidate.path);
23086
24842
  for (const discovered of parsed) {
23087
24843
  const key = `${discovered.host}:${discovered.serverName}:${discovered.sourcePath}`;
23088
24844
  if (seen.has(key)) continue;
@@ -23162,20 +24918,21 @@ function installedMcpFileCandidates(rootDir, homeDir) {
23162
24918
  { host: "opencode", path: resolve26(homeDir, ".config/opencode/opencode.json"), parser: "json" }
23163
24919
  ];
23164
24920
  }
23165
- function parseJsonMcpFile(path, host) {
24921
+ function parseJsonMcpFile2(path, host) {
23166
24922
  try {
23167
24923
  const raw = JSON.parse(readFileSync19(path, "utf-8"));
23168
- const servers = extractJsonMcpServers(raw, path, host);
24924
+ const servers = extractJsonMcpServers2(raw, path, host);
23169
24925
  return Object.entries(servers).flatMap(([serverName, config]) => {
23170
24926
  const normalized = normalizeCommonMcpServer(config, host);
23171
24927
  if (!normalized) return [];
23172
- return [toDiscovered(host, serverName, path, normalized.server, normalized.warnings)];
24928
+ const platformOverrides = buildNativeMcpPlatformOverrides(host, { [serverName]: config });
24929
+ return [toDiscovered(host, serverName, path, normalized.server, normalized.warnings, platformOverrides)];
23173
24930
  });
23174
24931
  } catch {
23175
24932
  return [];
23176
24933
  }
23177
24934
  }
23178
- function extractJsonMcpServers(raw, path, host) {
24935
+ function extractJsonMcpServers2(raw, path, host) {
23179
24936
  if (host === "opencode" && raw.mcp && typeof raw.mcp === "object") {
23180
24937
  return raw.mcp;
23181
24938
  }
@@ -23204,7 +24961,7 @@ function parseCodexTomlMcpFile(path) {
23204
24961
  let currentServer;
23205
24962
  let currentSubtable;
23206
24963
  for (const rawLine of text.split(/\r?\n/)) {
23207
- const line = stripTomlComment(rawLine).trim();
24964
+ const line = stripTomlComment2(rawLine).trim();
23208
24965
  if (!line) continue;
23209
24966
  const section = line.match(/^\[mcp_servers\.([A-Za-z0-9_.-]+)(?:\.([A-Za-z0-9_.-]+))?\]$/);
23210
24967
  if (section) {
@@ -23223,7 +24980,7 @@ function parseCodexTomlMcpFile(path) {
23223
24980
  const assignment = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
23224
24981
  if (!assignment) continue;
23225
24982
  const key = assignment[1];
23226
- const value = parseTomlValue(assignment[2].trim());
24983
+ const value = parseTomlValue2(assignment[2].trim());
23227
24984
  if (currentSubtable) {
23228
24985
  const table = servers[currentServer][currentSubtable];
23229
24986
  table[key] = value;
@@ -23234,7 +24991,8 @@ function parseCodexTomlMcpFile(path) {
23234
24991
  return Object.entries(servers).flatMap(([serverName, config]) => {
23235
24992
  const normalized = normalizeCommonMcpServer(config, "codex");
23236
24993
  if (!normalized) return [];
23237
- return [toDiscovered("codex", serverName, path, normalized.server, normalized.warnings)];
24994
+ const platformOverrides = buildNativeMcpPlatformOverrides("codex", { [serverName]: config });
24995
+ return [toDiscovered("codex", serverName, path, normalized.server, normalized.warnings, platformOverrides)];
23238
24996
  });
23239
24997
  }
23240
24998
  function normalizeCommonMcpServer(config, host) {
@@ -23242,7 +25000,7 @@ function normalizeCommonMcpServer(config, host) {
23242
25000
  const cfg = config;
23243
25001
  const warnings = [];
23244
25002
  const auth = inferAuth(cfg, warnings);
23245
- const remoteUrl = firstString(cfg.url, cfg.endpoint);
25003
+ const remoteUrl = firstString3(cfg.url, cfg.endpoint);
23246
25004
  if (remoteUrl) {
23247
25005
  const server = cfg.type === "sse" || cfg.transport === "sse" ? {
23248
25006
  transport: "sse",
@@ -23281,9 +25039,9 @@ function normalizeArgs(command2, args2, host) {
23281
25039
  if (host === "opencode" && Array.isArray(command2)) {
23282
25040
  return command2.slice(1).map(String);
23283
25041
  }
23284
- return normalizeStringArray(args2);
25042
+ return normalizeStringArray2(args2);
23285
25043
  }
23286
- function normalizeStringArray(value) {
25044
+ function normalizeStringArray2(value) {
23287
25045
  if (!Array.isArray(value)) return [];
23288
25046
  return value.map(String);
23289
25047
  }
@@ -23307,7 +25065,7 @@ function normalizeEnv(value, warnings) {
23307
25065
  return env;
23308
25066
  }
23309
25067
  function inferAuth(cfg, warnings) {
23310
- const bearerTokenEnv = firstString(cfg.bearer_token_env_var, cfg.bearerTokenEnvVar);
25068
+ const bearerTokenEnv = firstString3(cfg.bearer_token_env_var, cfg.bearerTokenEnvVar);
23311
25069
  if (bearerTokenEnv) {
23312
25070
  return {
23313
25071
  type: "bearer",
@@ -23333,13 +25091,13 @@ function inferAuth(cfg, warnings) {
23333
25091
  const headerEntries = Object.entries(headers);
23334
25092
  for (const [headerName, rawValue] of headerEntries) {
23335
25093
  if (typeof rawValue !== "string") continue;
23336
- const envVar = extractEnvVar(rawValue);
25094
+ const envVar = extractEnvVar3(rawValue);
23337
25095
  if (envVar) {
23338
25096
  return {
23339
25097
  type: headerName.toLowerCase() === "authorization" ? "bearer" : "header",
23340
25098
  envVar,
23341
25099
  headerName,
23342
- headerTemplate: rawValue.replace(envReferencePattern(envVar), "${value}")
25100
+ headerTemplate: rawValue.replace(envReferencePattern2(envVar), "${value}")
23343
25101
  };
23344
25102
  }
23345
25103
  if (looksSecretValue(rawValue)) {
@@ -23348,43 +25106,44 @@ function inferAuth(cfg, warnings) {
23348
25106
  }
23349
25107
  return void 0;
23350
25108
  }
23351
- function toDiscovered(host, serverName, sourcePath, server, warnings) {
25109
+ function toDiscovered(host, serverName, sourcePath, server, warnings, platformOverrides) {
23352
25110
  return {
23353
25111
  id: `${host}:${serverName}`,
23354
25112
  host,
23355
25113
  serverName,
23356
25114
  sourcePath,
23357
25115
  server,
23358
- warnings
25116
+ warnings,
25117
+ ...platformOverrides ? { platformOverrides } : {}
23359
25118
  };
23360
25119
  }
23361
- function firstString(...values) {
25120
+ function firstString3(...values) {
23362
25121
  for (const value of values) {
23363
25122
  if (typeof value === "string" && value.trim()) return value.trim();
23364
25123
  }
23365
25124
  return void 0;
23366
25125
  }
23367
25126
  function envPlaceholder(value) {
23368
- const envVar = extractEnvVar(value);
25127
+ const envVar = extractEnvVar3(value);
23369
25128
  return envVar ? `\${${envVar}}` : void 0;
23370
25129
  }
23371
- function extractEnvVar(value) {
25130
+ function extractEnvVar3(value) {
23372
25131
  const match = value.match(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/);
23373
25132
  return match?.[1] ?? match?.[2];
23374
25133
  }
23375
- function envReferencePattern(envVar) {
23376
- return new RegExp(`\\$\\{(?:env:)?${escapeRegExp2(envVar)}\\}|\\$${escapeRegExp2(envVar)}`);
25134
+ function envReferencePattern2(envVar) {
25135
+ return new RegExp(`\\$\\{(?:env:)?${escapeRegExp3(envVar)}\\}|\\$${escapeRegExp3(envVar)}`);
23377
25136
  }
23378
- function escapeRegExp2(value) {
25137
+ function escapeRegExp3(value) {
23379
25138
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23380
25139
  }
23381
25140
  function looksSecretKey(value) {
23382
25141
  return /(api|auth|secret|token|key|password|credential)/i.test(value);
23383
25142
  }
23384
25143
  function looksSecretValue(value) {
23385
- return value.length >= 16 && !extractEnvVar(value);
25144
+ return value.length >= 16 && !extractEnvVar3(value);
23386
25145
  }
23387
- function stripTomlComment(line) {
25146
+ function stripTomlComment2(line) {
23388
25147
  let inString = false;
23389
25148
  let quote2 = "";
23390
25149
  for (let i = 0; i < line.length; i += 1) {
@@ -23403,22 +25162,22 @@ function stripTomlComment(line) {
23403
25162
  }
23404
25163
  return line;
23405
25164
  }
23406
- function parseTomlValue(value) {
23407
- if (value.startsWith('"') && value.endsWith('"')) return unquoteTomlString(value);
25165
+ function parseTomlValue2(value) {
25166
+ if (value.startsWith('"') && value.endsWith('"')) return unquoteTomlString2(value);
23408
25167
  if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
23409
25168
  if (value.startsWith("[") && value.endsWith("]")) {
23410
25169
  const inner = value.slice(1, -1).trim();
23411
25170
  if (!inner) return [];
23412
- return splitTomlList(inner).map((part) => parseTomlValue(part.trim()));
25171
+ return splitTomlList2(inner).map((part) => parseTomlValue2(part.trim()));
23413
25172
  }
23414
25173
  if (value.startsWith("{") && value.endsWith("}")) {
23415
25174
  const inner = value.slice(1, -1).trim();
23416
25175
  const result = {};
23417
25176
  if (!inner) return result;
23418
- for (const part of splitTomlList(inner)) {
25177
+ for (const part of splitTomlList2(inner)) {
23419
25178
  const assignment = part.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
23420
25179
  if (!assignment) continue;
23421
- result[assignment[1]] = parseTomlValue(assignment[2].trim());
25180
+ result[assignment[1]] = parseTomlValue2(assignment[2].trim());
23422
25181
  }
23423
25182
  return result;
23424
25183
  }
@@ -23426,7 +25185,7 @@ function parseTomlValue(value) {
23426
25185
  if (value === "false") return false;
23427
25186
  return value;
23428
25187
  }
23429
- function splitTomlList(value) {
25188
+ function splitTomlList2(value) {
23430
25189
  const parts = [];
23431
25190
  let current = "";
23432
25191
  let inString = false;
@@ -23455,7 +25214,7 @@ function splitTomlList(value) {
23455
25214
  if (current.trim()) parts.push(current);
23456
25215
  return parts;
23457
25216
  }
23458
- function unquoteTomlString(value) {
25217
+ function unquoteTomlString2(value) {
23459
25218
  return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
23460
25219
  }
23461
25220
 
@@ -23616,7 +25375,7 @@ async function runUpgradeCommand() {
23616
25375
  }
23617
25376
  return;
23618
25377
  }
23619
- const install = spawnSync3(summary.command[0], summary.command.slice(1), runtime.jsonOutput ? {
25378
+ const install = spawnSync4(summary.command[0], summary.command.slice(1), runtime.jsonOutput ? {
23620
25379
  env: process.env,
23621
25380
  encoding: "utf-8",
23622
25381
  stdio: "pipe"
@@ -24686,7 +26445,8 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
24686
26445
  setupHints: sourcedContextPack.docsContext.setupHints,
24687
26446
  authHints: sourcedContextPack.docsContext.authHints,
24688
26447
  warnings: sourcedContextPack.docsContext.warnings
24689
- } : void 0
26448
+ } : void 0,
26449
+ platformOverrides: installedMcpSource?.platformOverrides
24690
26450
  });
24691
26451
  const createdFiles = plan.files.filter((file) => file.action === "create").map((file) => file.relativePath);
24692
26452
  const updatedFiles = plan.files.filter((file) => file.action === "update").map((file) => file.relativePath);