@orchid-labs/pluxx 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -4176,27 +4176,67 @@ function mergeAction(current, next) {
4176
4176
  return ACTION_PRIORITY[next] > ACTION_PRIORITY[current] ? next : current;
4177
4177
  }
4178
4178
  function permissionRulesNeedToolLevelDowngrade(permissions) {
4179
- return collectPermissionRules(permissions).some((rule) => rule.pattern !== "*");
4179
+ return collectPermissionRules(permissions).some((rule) => rule.kind === "MCP");
4180
4180
  }
4181
4181
  function buildOpenCodePermissionMap(permissions) {
4182
4182
  const rules = collectPermissionRules(permissions);
4183
4183
  const output = {};
4184
- const toolAliases = {
4185
- Bash: ["bash", "shell"],
4186
- Edit: ["edit", "write"],
4187
- Read: ["read"],
4188
- MCP: ["mcp"],
4189
- // OpenCode's native permission surface is tool-level and does not expose
4190
- // a dedicated skill permission key.
4191
- Skill: []
4192
- };
4193
4184
  for (const rule of rules) {
4194
- for (const tool of toolAliases[rule.kind]) {
4195
- output[tool] = mergeAction(output[tool], rule.action);
4185
+ if (rule.kind === "MCP") {
4186
+ const toolName = translateCanonicalMcpPermission(rule.pattern);
4187
+ if (!toolName) continue;
4188
+ output[toolName] = mergeScalarPermission(output[toolName], rule.action);
4189
+ continue;
4196
4190
  }
4191
+ const tool = toOpenCodePermissionTool(rule.kind);
4192
+ if (!tool) continue;
4193
+ output[tool] = mergePatternPermission(output[tool], rule.pattern, rule.action);
4197
4194
  }
4198
4195
  return output;
4199
4196
  }
4197
+ function toOpenCodePermissionTool(kind) {
4198
+ switch (kind) {
4199
+ case "Bash":
4200
+ return "bash";
4201
+ case "Edit":
4202
+ return "edit";
4203
+ case "Read":
4204
+ return "read";
4205
+ case "Skill":
4206
+ return "skill";
4207
+ case "MCP":
4208
+ return null;
4209
+ }
4210
+ }
4211
+ function mergeScalarPermission(current, next) {
4212
+ if (!current) return next;
4213
+ if (typeof current === "string") {
4214
+ return mergeAction(current, next);
4215
+ }
4216
+ const merged = { ...current };
4217
+ merged["*"] = mergeAction(merged["*"], next);
4218
+ return merged;
4219
+ }
4220
+ function mergePatternPermission(current, pattern, next) {
4221
+ if (pattern === "*") {
4222
+ return mergeScalarPermission(current, next);
4223
+ }
4224
+ const merged = typeof current === "string" ? { "*": current } : { ...current ?? {} };
4225
+ merged[pattern] = mergeAction(merged[pattern], next);
4226
+ return merged;
4227
+ }
4228
+ function translateCanonicalMcpPermission(pattern) {
4229
+ const trimmed = pattern.trim();
4230
+ if (!trimmed || trimmed === "*") return null;
4231
+ const dot = trimmed.indexOf(".");
4232
+ if (dot === -1) {
4233
+ return `${trimmed}_*`;
4234
+ }
4235
+ const server = trimmed.slice(0, dot).trim();
4236
+ const tool = trimmed.slice(dot + 1).trim();
4237
+ if (!server || !tool) return null;
4238
+ return `${server}_${tool.replace(/\./g, "_")}`;
4239
+ }
4200
4240
  function buildGeneratedPermissionHookScript(permissions) {
4201
4241
  const rules = collectPermissionRules(permissions);
4202
4242
  if (rules.length === 0) return null;
@@ -4334,6 +4374,7 @@ function claudeResponse(match) {
4334
4374
  if (!match) return {};
4335
4375
  return {
4336
4376
  hookSpecificOutput: {
4377
+ hookEventName: "PreToolUse",
4337
4378
  permissionDecision: match.action,
4338
4379
  permissionDecisionReason: "Pluxx permissions matched " + match.rule.raw,
4339
4380
  },
@@ -4726,7 +4767,7 @@ async function loadConfig(dir = process.cwd()) {
4726
4767
  }
4727
4768
 
4728
4769
  // src/generators/index.ts
4729
- import { rmSync, mkdirSync as mkdirSync2 } from "fs";
4770
+ import { rmSync, mkdirSync as mkdirSync3 } from "fs";
4730
4771
  import { resolve as resolve8, relative as relative5 } from "path";
4731
4772
 
4732
4773
  // src/generators/base.ts
@@ -4866,9 +4907,9 @@ var Generator = class {
4866
4907
  for (const configPath of this.config.passthrough ?? []) {
4867
4908
  const src = this.resolveConfigPath(configPath, "passthrough");
4868
4909
  if (!existsSync4(src)) continue;
4869
- const basename8 = src.split("/").filter(Boolean).pop();
4870
- if (!basename8) continue;
4871
- this.copyDir(configPath, `${basename8}/`, "passthrough");
4910
+ const basename9 = src.split("/").filter(Boolean).pop();
4911
+ if (!basename9) continue;
4912
+ this.copyDir(configPath, `${basename9}/`, "passthrough");
4872
4913
  }
4873
4914
  }
4874
4915
  /** Build canonical MCP server configs for target-specific output shaping. */
@@ -4945,8 +4986,8 @@ var Generator = class {
4945
4986
  };
4946
4987
 
4947
4988
  // src/generators/shared/claude-family.ts
4948
- import { existsSync as existsSync5 } from "fs";
4949
- import { resolve as resolve4 } from "path";
4989
+ import { existsSync as existsSync6 } from "fs";
4990
+ import { resolve as resolve5 } from "path";
4950
4991
 
4951
4992
  // src/generators/hooks-warning.ts
4952
4993
  var MATCHER_PASSTHROUGH_PLATFORMS = /* @__PURE__ */ new Set([
@@ -5019,6 +5060,118 @@ function mapHookEventToPascalCase(event) {
5019
5060
  return PASCAL_CASE_HOOK_ALIASES[event] ?? event.charAt(0).toUpperCase() + event.slice(1);
5020
5061
  }
5021
5062
 
5063
+ // src/agents.ts
5064
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
5065
+ import { basename, resolve as resolve4 } from "path";
5066
+ function firstHeading(content) {
5067
+ const lines = content.split(/\r?\n/);
5068
+ for (const line of lines) {
5069
+ const match = line.match(/^#\s+(.*)$/);
5070
+ if (match?.[1]?.trim()) return match[1].trim();
5071
+ }
5072
+ return void 0;
5073
+ }
5074
+ function splitMarkdownFrontmatter(content) {
5075
+ const lines = content.split(/\r?\n/);
5076
+ if (lines[0]?.trim() !== "---") {
5077
+ return {
5078
+ frontmatterLines: [],
5079
+ body: content
5080
+ };
5081
+ }
5082
+ let endIndex = -1;
5083
+ for (let i = 1; i < lines.length; i += 1) {
5084
+ if (lines[i].trim() === "---") {
5085
+ endIndex = i;
5086
+ break;
5087
+ }
5088
+ }
5089
+ if (endIndex === -1) {
5090
+ return {
5091
+ frontmatterLines: [],
5092
+ body: content
5093
+ };
5094
+ }
5095
+ return {
5096
+ frontmatterLines: lines.slice(1, endIndex),
5097
+ body: lines.slice(endIndex + 1).join("\n")
5098
+ };
5099
+ }
5100
+ function parseScalarValue(raw) {
5101
+ const trimmed = raw.trim();
5102
+ if (trimmed === "true") return true;
5103
+ if (trimmed === "false") return false;
5104
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
5105
+ if (trimmed.length >= 2) {
5106
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
5107
+ return trimmed.slice(1, -1);
5108
+ }
5109
+ }
5110
+ return trimmed;
5111
+ }
5112
+ function parseAgentFrontmatter(frontmatterLines) {
5113
+ const root = {};
5114
+ const stack = [
5115
+ { indent: -1, target: root }
5116
+ ];
5117
+ for (const line of frontmatterLines) {
5118
+ if (!line.trim() || line.trim().startsWith("#")) continue;
5119
+ const match = line.match(/^(\s*)(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_.-]+))\s*:\s*(.*)$/);
5120
+ if (!match) continue;
5121
+ const indent = match[1].length;
5122
+ const key = match[2] ?? match[3] ?? match[4];
5123
+ const rawValue = match[5].trim();
5124
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
5125
+ stack.pop();
5126
+ }
5127
+ const parent = stack[stack.length - 1].target;
5128
+ if (!rawValue) {
5129
+ const nested = {};
5130
+ parent[key] = nested;
5131
+ stack.push({ indent, target: nested });
5132
+ continue;
5133
+ }
5134
+ parent[key] = parseScalarValue(rawValue);
5135
+ }
5136
+ return root;
5137
+ }
5138
+ function walkMarkdownFiles(dir) {
5139
+ const entries = readdirSync(dir);
5140
+ const files = [];
5141
+ for (const entry of entries) {
5142
+ const fullPath = resolve4(dir, entry);
5143
+ const stat = statSync(fullPath);
5144
+ if (stat.isDirectory()) {
5145
+ files.push(...walkMarkdownFiles(fullPath));
5146
+ continue;
5147
+ }
5148
+ if (stat.isFile() && entry.endsWith(".md")) {
5149
+ files.push(fullPath);
5150
+ }
5151
+ }
5152
+ return files;
5153
+ }
5154
+ function parseCanonicalAgentFile(agentPath) {
5155
+ const content = readFileSync2(agentPath, "utf-8");
5156
+ const { frontmatterLines, body } = splitMarkdownFrontmatter(content);
5157
+ const frontmatter = parseAgentFrontmatter(frontmatterLines);
5158
+ const fileStem = basename(agentPath, ".md");
5159
+ const name = typeof frontmatter.name === "string" && frontmatter.name ? frontmatter.name : fileStem;
5160
+ const description = typeof frontmatter.description === "string" && frontmatter.description ? frontmatter.description : firstHeading(body);
5161
+ return {
5162
+ filePath: agentPath,
5163
+ fileStem,
5164
+ name,
5165
+ description,
5166
+ body: body.trim(),
5167
+ frontmatter
5168
+ };
5169
+ }
5170
+ function readCanonicalAgentFiles(agentsDir) {
5171
+ if (!agentsDir || !existsSync5(agentsDir)) return [];
5172
+ return walkMarkdownFiles(agentsDir).sort((a, b) => a.localeCompare(b)).map(parseCanonicalAgentFile);
5173
+ }
5174
+
5022
5175
  // src/generators/shared/claude-family.ts
5023
5176
  async function generateClaudeFamilyOutputs(args2) {
5024
5177
  const {
@@ -5030,13 +5183,13 @@ async function generateClaudeFamilyOutputs(args2) {
5030
5183
  writeFile: writeFile3
5031
5184
  } = args2;
5032
5185
  await Promise.all([
5033
- writeManifest(config, options, writeJson),
5186
+ writeManifest(config, rootDir, options, writeJson),
5034
5187
  writeMcpConfig(config, platform, writeJson),
5035
5188
  writeHooks(config, platform, options, writeJson, writeFile3),
5036
5189
  writeInstructions(config, rootDir, options, writeFile3)
5037
5190
  ]);
5038
5191
  }
5039
- async function writeManifest(config, options, writeJson) {
5192
+ async function writeManifest(config, rootDir, options, writeJson) {
5040
5193
  const manifest = {
5041
5194
  name: config.name,
5042
5195
  version: config.version,
@@ -5053,8 +5206,15 @@ async function writeManifest(config, options, writeJson) {
5053
5206
  if (config.commands) {
5054
5207
  manifest.commands = "./commands/";
5055
5208
  }
5056
- if (config.agents) {
5209
+ const agentsManifestMode = options.agentsManifestMode ?? "directory";
5210
+ if (config.agents && agentsManifestMode === "directory") {
5057
5211
  manifest.agents = "./agents/";
5212
+ } else if (config.agents && agentsManifestMode === "files") {
5213
+ const agentsDir = resolve5(rootDir, config.agents);
5214
+ const agents = readCanonicalAgentFiles(agentsDir);
5215
+ if (agents.length > 0) {
5216
+ manifest.agents = agents.map((agent) => `./agents/${agent.fileStem}.md`);
5217
+ }
5058
5218
  }
5059
5219
  manifest.skills = "./skills/";
5060
5220
  if ((config.hooks || config.permissions) && options.includeStandardHooksManifest !== false) {
@@ -5149,8 +5309,8 @@ async function writeHooks(config, platform, options, writeJson, writeFile3) {
5149
5309
  }
5150
5310
  async function writeInstructions(config, rootDir, options, writeFile3) {
5151
5311
  if (!config.instructions) return;
5152
- const srcPath = resolve4(rootDir, config.instructions);
5153
- if (!existsSync5(srcPath)) return;
5312
+ const srcPath = resolve5(rootDir, config.instructions);
5313
+ if (!existsSync6(srcPath)) return;
5154
5314
  const content = await readTextFile(srcPath);
5155
5315
  const titleSuffix = options.titleSuffix ?? "Plugin";
5156
5316
  const instructions = [
@@ -5167,8 +5327,49 @@ function defaultMapEventName(event) {
5167
5327
  }
5168
5328
 
5169
5329
  // src/generators/claude-code/index.ts
5170
- import { existsSync as existsSync6, readFileSync as readFileSync2, readdirSync, writeFileSync } from "fs";
5171
- import { basename, join as join2 } from "path";
5330
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
5331
+ import { basename as basename2, join as join2 } from "path";
5332
+
5333
+ // src/delegation.ts
5334
+ function getPortableDelegationProfile(frontmatter) {
5335
+ const permission = asMap(frontmatter.permission);
5336
+ const bash = asMap(permission?.bash);
5337
+ const task = asMap(permission?.task);
5338
+ return {
5339
+ mode: typeof frontmatter.mode === "string" ? frontmatter.mode : void 0,
5340
+ hidden: frontmatter.hidden === true,
5341
+ editPolicy: typeof permission?.edit === "string" ? permission.edit : void 0,
5342
+ bashPolicy: typeof bash?.["*"] === "string" ? bash["*"] : void 0,
5343
+ taskPolicy: typeof task?.["*"] === "string" ? task["*"] : void 0
5344
+ };
5345
+ }
5346
+ function buildDelegationBehaviorNotes(frontmatter) {
5347
+ const profile = getPortableDelegationProfile(frontmatter);
5348
+ const notes = [];
5349
+ if (profile.mode === "subagent" || profile.hidden) {
5350
+ notes.push("This specialist is intended primarily for delegated use rather than as the default top-level worker.");
5351
+ }
5352
+ if (profile.editPolicy === "deny") {
5353
+ notes.push("Stay read-only unless the parent task explicitly asks for file edits.");
5354
+ }
5355
+ if (profile.bashPolicy === "deny") {
5356
+ notes.push("Avoid shell commands unless the parent task explicitly requires them.");
5357
+ } else if (profile.bashPolicy === "ask") {
5358
+ notes.push("Use shell commands sparingly and only when they are clearly necessary to complete the task.");
5359
+ }
5360
+ if (profile.taskPolicy === "deny") {
5361
+ notes.push("Do not delegate further subtasks unless the parent task explicitly asks for additional specialist work.");
5362
+ } else if (profile.taskPolicy === "ask") {
5363
+ notes.push("Only delegate further subtasks when the work clearly benefits from another specialist.");
5364
+ }
5365
+ return notes;
5366
+ }
5367
+ function asMap(value) {
5368
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
5369
+ return value;
5370
+ }
5371
+
5372
+ // src/generators/claude-code/index.ts
5172
5373
  var ClaudeCodeGenerator = class extends Generator {
5173
5374
  platform = "claude-code";
5174
5375
  async generate() {
@@ -5180,7 +5381,8 @@ var ClaudeCodeGenerator = class extends Generator {
5180
5381
  manifestPath: ".claude-plugin/plugin.json",
5181
5382
  instructionsFile: "CLAUDE.md",
5182
5383
  pluginRootVar: "CLAUDE_PLUGIN_ROOT",
5183
- includeStandardHooksManifest: false
5384
+ includeStandardHooksManifest: false,
5385
+ agentsManifestMode: "files"
5184
5386
  },
5185
5387
  writeJson: (relativePath, data) => this.writeJson(relativePath, data),
5186
5388
  writeFile: (relativePath, content) => this.writeFile(relativePath, content)
@@ -5195,29 +5397,108 @@ var ClaudeCodeGenerator = class extends Generator {
5195
5397
  copySkills() {
5196
5398
  super.copySkills();
5197
5399
  const collidingSkills = this.collectCollidingSkills();
5400
+ const wrappedSkills = this.collectCommandWrappedSkills();
5198
5401
  for (const skill of collidingSkills) {
5199
5402
  const outputPath = join2(this.outDir, "skills", skill.dirName, "SKILL.md");
5200
- if (!existsSync6(outputPath)) continue;
5201
- const current = readFileSync2(outputPath, "utf-8");
5403
+ if (!existsSync7(outputPath)) continue;
5404
+ const current = readFileSync3(outputPath, "utf-8");
5202
5405
  const hiddenName = buildHiddenSkillName(skill.effectiveName);
5203
- const rewritten = rewriteClaudeCollidingSkill(current, hiddenName);
5406
+ const rewritten = rewriteClaudeSkillVisibility(current, {
5407
+ nameOverride: hiddenName,
5408
+ userInvocable: false
5409
+ });
5410
+ if (rewritten !== current) {
5411
+ writeFileSync(outputPath, rewritten, "utf-8");
5412
+ }
5413
+ }
5414
+ for (const skill of wrappedSkills) {
5415
+ if (collidingSkills.some((entry) => entry.dirName === skill.dirName)) continue;
5416
+ const outputPath = join2(this.outDir, "skills", skill.dirName, "SKILL.md");
5417
+ if (!existsSync7(outputPath)) continue;
5418
+ const current = readFileSync3(outputPath, "utf-8");
5419
+ const rewritten = rewriteClaudeSkillVisibility(current, {
5420
+ userInvocable: false
5421
+ });
5204
5422
  if (rewritten !== current) {
5205
5423
  writeFileSync(outputPath, rewritten, "utf-8");
5206
5424
  }
5207
5425
  }
5208
5426
  }
5427
+ copyAgents() {
5428
+ if (!this.config.agents) return;
5429
+ const agentsDir = this.resolveConfigPath(this.config.agents, "agents");
5430
+ const agents = readCanonicalAgentFiles(agentsDir);
5431
+ if (agents.length === 0) return;
5432
+ mkdirSync2(join2(this.outDir, "agents"), { recursive: true });
5433
+ for (const agent of agents) {
5434
+ const frontmatter = [
5435
+ "---",
5436
+ `name: ${JSON.stringify(agent.name)}`,
5437
+ `description: ${JSON.stringify(agent.description ?? `${agent.name} specialist.`)}`
5438
+ ];
5439
+ if (typeof agent.frontmatter.model === "string" && agent.frontmatter.model) {
5440
+ frontmatter.push(`model: ${JSON.stringify(agent.frontmatter.model)}`);
5441
+ }
5442
+ const effort = typeof agent.frontmatter.model_reasoning_effort === "string" && agent.frontmatter.model_reasoning_effort ? agent.frontmatter.model_reasoning_effort : typeof agent.frontmatter.effort === "string" && agent.frontmatter.effort ? agent.frontmatter.effort : void 0;
5443
+ if (effort) {
5444
+ frontmatter.push(`effort: ${JSON.stringify(effort)}`);
5445
+ }
5446
+ const maxTurns = typeof agent.frontmatter.maxTurns === "number" ? agent.frontmatter.maxTurns : typeof agent.frontmatter.steps === "number" ? agent.frontmatter.steps : typeof agent.frontmatter.maxSteps === "number" ? agent.frontmatter.maxSteps : void 0;
5447
+ if (typeof maxTurns === "number") {
5448
+ frontmatter.push(`maxTurns: ${maxTurns}`);
5449
+ }
5450
+ const claudeTools = selectClaudeToolsField(agent.frontmatter);
5451
+ if (claudeTools) {
5452
+ frontmatter.push(`tools: ${claudeTools}`);
5453
+ }
5454
+ const disallowedTools = buildClaudeDisallowedTools(agent.frontmatter);
5455
+ if (disallowedTools.length > 0) {
5456
+ frontmatter.push(`disallowedTools: ${disallowedTools.join(", ")}`);
5457
+ }
5458
+ if (typeof agent.frontmatter.skills === "string" && agent.frontmatter.skills.trim()) {
5459
+ frontmatter.push(`skills: ${agent.frontmatter.skills}`);
5460
+ }
5461
+ if (typeof agent.frontmatter.memory === "string" && agent.frontmatter.memory.trim()) {
5462
+ frontmatter.push(`memory: ${JSON.stringify(agent.frontmatter.memory)}`);
5463
+ }
5464
+ if (typeof agent.frontmatter.background === "boolean") {
5465
+ frontmatter.push(`background: ${agent.frontmatter.background}`);
5466
+ }
5467
+ if (typeof agent.frontmatter.isolation === "string" && agent.frontmatter.isolation.trim()) {
5468
+ frontmatter.push(`isolation: ${JSON.stringify(agent.frontmatter.isolation)}`);
5469
+ }
5470
+ if (typeof agent.frontmatter.color === "string" && agent.frontmatter.color.trim()) {
5471
+ frontmatter.push(`color: ${JSON.stringify(agent.frontmatter.color)}`);
5472
+ }
5473
+ frontmatter.push("---");
5474
+ const delegationNotes = buildDelegationBehaviorNotes(agent.frontmatter);
5475
+ const bodyParts = [
5476
+ ...delegationNotes.length > 0 ? [
5477
+ "Delegation contract:",
5478
+ ...delegationNotes.map((note) => `- ${note}`),
5479
+ ""
5480
+ ] : [],
5481
+ agent.body
5482
+ ].filter(Boolean);
5483
+ const outputPath = join2(this.outDir, "agents", `${agent.fileStem}.md`);
5484
+ writeFileSync(outputPath, `${frontmatter.join("\n")}
5485
+
5486
+ ${bodyParts.join("\n").trim()}
5487
+ `, "utf-8");
5488
+ }
5489
+ }
5209
5490
  collectCollidingSkills() {
5210
5491
  if (!this.config.commands) return [];
5211
5492
  const commandsSrc = this.resolveConfigPath(this.config.commands, "commands");
5212
5493
  const skillsSrc = this.resolveConfigPath(this.config.skills, "skills");
5213
- if (!existsSync6(commandsSrc) || !existsSync6(skillsSrc)) return [];
5494
+ if (!existsSync7(commandsSrc) || !existsSync7(skillsSrc)) return [];
5214
5495
  const commandNames = collectTopLevelCommandNames(commandsSrc);
5215
5496
  const collidingSkills = [];
5216
- for (const entry of readdirSync(skillsSrc, { withFileTypes: true })) {
5497
+ for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
5217
5498
  if (!entry.isDirectory()) continue;
5218
5499
  const skillFile = join2(skillsSrc, entry.name, "SKILL.md");
5219
- if (!existsSync6(skillFile)) continue;
5220
- const content = readFileSync2(skillFile, "utf-8");
5500
+ if (!existsSync7(skillFile)) continue;
5501
+ const content = readFileSync3(skillFile, "utf-8");
5221
5502
  const effectiveName = getEffectiveSkillName(content, entry.name);
5222
5503
  if (commandNames.has(effectiveName)) {
5223
5504
  collidingSkills.push({ dirName: entry.name, effectiveName });
@@ -5225,16 +5506,48 @@ var ClaudeCodeGenerator = class extends Generator {
5225
5506
  }
5226
5507
  return collidingSkills;
5227
5508
  }
5509
+ collectCommandWrappedSkills() {
5510
+ if (!this.config.commands) return [];
5511
+ const commandsSrc = this.resolveConfigPath(this.config.commands, "commands");
5512
+ const skillsSrc = this.resolveConfigPath(this.config.skills, "skills");
5513
+ if (!existsSync7(commandsSrc) || !existsSync7(skillsSrc)) return [];
5514
+ const referencedSkills = collectWrappedSkillNames(commandsSrc);
5515
+ if (referencedSkills.size === 0) return [];
5516
+ const wrappedSkills = [];
5517
+ for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
5518
+ if (!entry.isDirectory()) continue;
5519
+ const skillFile = join2(skillsSrc, entry.name, "SKILL.md");
5520
+ if (!existsSync7(skillFile)) continue;
5521
+ const content = readFileSync3(skillFile, "utf-8");
5522
+ const effectiveName = getEffectiveSkillName(content, entry.name);
5523
+ if (referencedSkills.has(effectiveName)) {
5524
+ wrappedSkills.push({ dirName: entry.name, effectiveName });
5525
+ }
5526
+ }
5527
+ return wrappedSkills;
5528
+ }
5228
5529
  };
5229
5530
  function collectTopLevelCommandNames(commandsRoot) {
5230
5531
  const commandNames = /* @__PURE__ */ new Set();
5231
- for (const entry of readdirSync(commandsRoot, { withFileTypes: true })) {
5532
+ for (const entry of readdirSync2(commandsRoot, { withFileTypes: true })) {
5232
5533
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
5233
- commandNames.add(basename(entry.name, ".md"));
5534
+ commandNames.add(basename2(entry.name, ".md"));
5234
5535
  }
5235
5536
  }
5236
5537
  return commandNames;
5237
5538
  }
5539
+ function collectWrappedSkillNames(commandsRoot) {
5540
+ const wrappedSkills = /* @__PURE__ */ new Set();
5541
+ for (const entry of readdirSync2(commandsRoot, { withFileTypes: true })) {
5542
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
5543
+ const content = readFileSync3(join2(commandsRoot, entry.name), "utf-8");
5544
+ for (const match of content.matchAll(/Use the `([^`]+)` skill\./g)) {
5545
+ const skillName = match[1]?.trim();
5546
+ if (skillName) wrappedSkills.add(skillName);
5547
+ }
5548
+ }
5549
+ return wrappedSkills;
5550
+ }
5238
5551
  function getEffectiveSkillName(content, fallback) {
5239
5552
  const frontmatter = extractFrontmatterLines(content);
5240
5553
  if (!frontmatter) return fallback;
@@ -5251,13 +5564,14 @@ function buildHiddenSkillName(name) {
5251
5564
  const trimmed = name.length > maxBaseLength ? name.slice(0, maxBaseLength) : name;
5252
5565
  return `${trimmed}-skill`;
5253
5566
  }
5254
- function rewriteClaudeCollidingSkill(content, hiddenName) {
5567
+ function rewriteClaudeSkillVisibility(content, options) {
5255
5568
  const frontmatter = extractFrontmatterLines(content);
5256
5569
  if (!frontmatter) {
5570
+ const generatedFrontmatter = ["---"];
5571
+ if (options.nameOverride) generatedFrontmatter.push(`name: ${options.nameOverride}`);
5572
+ if (options.userInvocable === false) generatedFrontmatter.push("user-invocable: false");
5257
5573
  return [
5258
- "---",
5259
- `name: ${hiddenName}`,
5260
- "user-invocable: false",
5574
+ ...generatedFrontmatter,
5261
5575
  "---",
5262
5576
  "",
5263
5577
  content.trimStart()
@@ -5268,18 +5582,18 @@ function rewriteClaudeCollidingSkill(content, hiddenName) {
5268
5582
  let sawUserInvocable = false;
5269
5583
  for (let index = 0; index < rewritten.length; index += 1) {
5270
5584
  const trimmed = rewritten[index].trim();
5271
- if (/^name:\s*/i.test(trimmed)) {
5272
- rewritten[index] = `name: ${hiddenName}`;
5585
+ if (options.nameOverride && /^name:\s*/i.test(trimmed)) {
5586
+ rewritten[index] = `name: ${options.nameOverride}`;
5273
5587
  sawName = true;
5274
5588
  continue;
5275
5589
  }
5276
- if (/^user-invocable:\s*/i.test(trimmed)) {
5590
+ if (options.userInvocable === false && /^user-invocable:\s*/i.test(trimmed)) {
5277
5591
  rewritten[index] = "user-invocable: false";
5278
5592
  sawUserInvocable = true;
5279
5593
  }
5280
5594
  }
5281
- if (!sawName) rewritten.push(`name: ${hiddenName}`);
5282
- if (!sawUserInvocable) rewritten.push("user-invocable: false");
5595
+ if (options.nameOverride && !sawName) rewritten.push(`name: ${options.nameOverride}`);
5596
+ if (options.userInvocable === false && !sawUserInvocable) rewritten.push("user-invocable: false");
5283
5597
  const lines = content.split("\n");
5284
5598
  const endIndex = findFrontmatterEndIndex(lines);
5285
5599
  const body = endIndex === -1 ? content : lines.slice(endIndex + 1).join("\n");
@@ -5308,162 +5622,51 @@ function stripYamlScalar(value) {
5308
5622
  }
5309
5623
  return trimmed;
5310
5624
  }
5311
-
5312
- // src/generators/cursor/index.ts
5313
- import { existsSync as existsSync8 } from "fs";
5314
-
5315
- // src/agents.ts
5316
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
5317
- import { basename as basename2, resolve as resolve5 } from "path";
5318
- function firstHeading(content) {
5319
- const lines = content.split(/\r?\n/);
5320
- for (const line of lines) {
5321
- const match = line.match(/^#\s+(.*)$/);
5322
- if (match?.[1]?.trim()) return match[1].trim();
5323
- }
5324
- return void 0;
5325
- }
5326
- function splitMarkdownFrontmatter(content) {
5327
- const lines = content.split(/\r?\n/);
5328
- if (lines[0]?.trim() !== "---") {
5329
- return {
5330
- frontmatterLines: [],
5331
- body: content
5332
- };
5333
- }
5334
- let endIndex = -1;
5335
- for (let i = 1; i < lines.length; i += 1) {
5336
- if (lines[i].trim() === "---") {
5337
- endIndex = i;
5338
- break;
5339
- }
5625
+ function buildClaudeDisallowedTools(frontmatter) {
5626
+ const tools = /* @__PURE__ */ new Set();
5627
+ const permission = asMap2(frontmatter.permission);
5628
+ const bash = asMap2(permission?.bash);
5629
+ const legacyTools = asMap2(frontmatter.tools);
5630
+ if (permission?.edit === "deny") {
5631
+ tools.add("Write");
5632
+ tools.add("Edit");
5633
+ tools.add("MultiEdit");
5340
5634
  }
5341
- if (endIndex === -1) {
5342
- return {
5343
- frontmatterLines: [],
5344
- body: content
5345
- };
5635
+ if (permission?.bash === "deny" || bash?.["*"] === "deny") {
5636
+ tools.add("Bash");
5346
5637
  }
5347
- return {
5348
- frontmatterLines: lines.slice(1, endIndex),
5349
- body: lines.slice(endIndex + 1).join("\n")
5350
- };
5351
- }
5352
- function parseScalarValue(raw) {
5353
- const trimmed = raw.trim();
5354
- if (trimmed === "true") return true;
5355
- if (trimmed === "false") return false;
5356
- if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
5357
- if (trimmed.length >= 2) {
5358
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
5359
- return trimmed.slice(1, -1);
5360
- }
5638
+ if (legacyTools?.write === false || legacyTools?.edit === false || legacyTools?.patch === false || legacyTools?.multiedit === false) {
5639
+ tools.add("Write");
5640
+ tools.add("Edit");
5641
+ tools.add("MultiEdit");
5361
5642
  }
5362
- return trimmed;
5363
- }
5364
- function parseAgentFrontmatter(frontmatterLines) {
5365
- const root = {};
5366
- const stack = [
5367
- { indent: -1, target: root }
5368
- ];
5369
- for (const line of frontmatterLines) {
5370
- if (!line.trim() || line.trim().startsWith("#")) continue;
5371
- const match = line.match(/^(\s*)(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_.-]+))\s*:\s*(.*)$/);
5372
- if (!match) continue;
5373
- const indent = match[1].length;
5374
- const key = match[2] ?? match[3] ?? match[4];
5375
- const rawValue = match[5].trim();
5376
- while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
5377
- stack.pop();
5378
- }
5379
- const parent = stack[stack.length - 1].target;
5380
- if (!rawValue) {
5381
- const nested = {};
5382
- parent[key] = nested;
5383
- stack.push({ indent, target: nested });
5384
- continue;
5385
- }
5386
- parent[key] = parseScalarValue(rawValue);
5643
+ if (legacyTools?.bash === false || legacyTools?.shell === false) {
5644
+ tools.add("Bash");
5387
5645
  }
5388
- return root;
5389
- }
5390
- function walkMarkdownFiles(dir) {
5391
- const entries = readdirSync2(dir);
5392
- const files = [];
5393
- for (const entry of entries) {
5394
- const fullPath = resolve5(dir, entry);
5395
- const stat = statSync(fullPath);
5396
- if (stat.isDirectory()) {
5397
- files.push(...walkMarkdownFiles(fullPath));
5398
- continue;
5399
- }
5400
- if (stat.isFile() && entry.endsWith(".md")) {
5401
- files.push(fullPath);
5646
+ if (typeof frontmatter.disallowedTools === "string") {
5647
+ for (const token of frontmatter.disallowedTools.split(",")) {
5648
+ const trimmed = token.trim();
5649
+ if (trimmed) tools.add(trimmed);
5402
5650
  }
5403
5651
  }
5404
- return files;
5405
- }
5406
- function parseCanonicalAgentFile(agentPath) {
5407
- const content = readFileSync3(agentPath, "utf-8");
5408
- const { frontmatterLines, body } = splitMarkdownFrontmatter(content);
5409
- const frontmatter = parseAgentFrontmatter(frontmatterLines);
5410
- const fileStem = basename2(agentPath, ".md");
5411
- const name = typeof frontmatter.name === "string" && frontmatter.name ? frontmatter.name : fileStem;
5412
- const description = typeof frontmatter.description === "string" && frontmatter.description ? frontmatter.description : firstHeading(body);
5413
- return {
5414
- filePath: agentPath,
5415
- fileStem,
5416
- name,
5417
- description,
5418
- body: body.trim(),
5419
- frontmatter
5420
- };
5421
- }
5422
- function readCanonicalAgentFiles(agentsDir) {
5423
- if (!agentsDir || !existsSync7(agentsDir)) return [];
5424
- return walkMarkdownFiles(agentsDir).sort((a, b) => a.localeCompare(b)).map(parseCanonicalAgentFile);
5425
- }
5426
-
5427
- // src/delegation.ts
5428
- function getPortableDelegationProfile(frontmatter) {
5429
- const permission = asMap(frontmatter.permission);
5430
- const bash = asMap(permission?.bash);
5431
- const task = asMap(permission?.task);
5432
- return {
5433
- mode: typeof frontmatter.mode === "string" ? frontmatter.mode : void 0,
5434
- hidden: frontmatter.hidden === true,
5435
- editPolicy: typeof permission?.edit === "string" ? permission.edit : void 0,
5436
- bashPolicy: typeof bash?.["*"] === "string" ? bash["*"] : void 0,
5437
- taskPolicy: typeof task?.["*"] === "string" ? task["*"] : void 0
5438
- };
5652
+ return Array.from(tools);
5439
5653
  }
5440
- function buildDelegationBehaviorNotes(frontmatter) {
5441
- const profile = getPortableDelegationProfile(frontmatter);
5442
- const notes = [];
5443
- if (profile.mode === "subagent" || profile.hidden) {
5444
- notes.push("This specialist is intended primarily for delegated use rather than as the default top-level worker.");
5445
- }
5446
- if (profile.editPolicy === "deny") {
5447
- notes.push("Stay read-only unless the parent task explicitly asks for file edits.");
5448
- }
5449
- if (profile.bashPolicy === "deny") {
5450
- notes.push("Avoid shell commands unless the parent task explicitly requires them.");
5451
- } else if (profile.bashPolicy === "ask") {
5452
- notes.push("Use shell commands sparingly and only when they are clearly necessary to complete the task.");
5453
- }
5454
- if (profile.taskPolicy === "deny") {
5455
- notes.push("Do not delegate further subtasks unless the parent task explicitly asks for additional specialist work.");
5456
- } else if (profile.taskPolicy === "ask") {
5457
- notes.push("Only delegate further subtasks when the work clearly benefits from another specialist.");
5654
+ function selectClaudeToolsField(frontmatter) {
5655
+ if (typeof frontmatter.tools !== "string") return null;
5656
+ const tools = frontmatter.tools.split(",").map((token) => token.trim()).filter(Boolean);
5657
+ if (tools.length === 0) return null;
5658
+ if (tools.some((token) => token.startsWith("mcp__"))) {
5659
+ return null;
5458
5660
  }
5459
- return notes;
5661
+ return tools.join(", ");
5460
5662
  }
5461
- function asMap(value) {
5663
+ function asMap2(value) {
5462
5664
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
5463
5665
  return value;
5464
5666
  }
5465
5667
 
5466
5668
  // src/generators/cursor/index.ts
5669
+ import { existsSync as existsSync8 } from "fs";
5467
5670
  var CursorGenerator = class extends Generator {
5468
5671
  platform = "cursor";
5469
5672
  async generate() {
@@ -5548,7 +5751,9 @@ var CursorGenerator = class extends Generator {
5548
5751
  if (entry.timeout) hookDef.timeout = entry.timeout;
5549
5752
  if (entry.matcher) hookDef.matcher = entry.matcher;
5550
5753
  if (entry.failClosed) hookDef.failClosed = entry.failClosed;
5551
- if (entry.loop_limit !== void 0) hookDef.loop_limit = entry.loop_limit;
5754
+ if (entry.loop_limit !== void 0 && CURSOR_LOOP_LIMIT_HOOK_EVENTS.includes(event)) {
5755
+ hookDef.loop_limit = entry.loop_limit;
5756
+ }
5552
5757
  return hookDef;
5553
5758
  })
5554
5759
  ];
@@ -5682,6 +5887,23 @@ function parseCommandFrontmatterDescription(frontmatterLines) {
5682
5887
  }
5683
5888
  return void 0;
5684
5889
  }
5890
+ function parseCommandFrontmatterString(frontmatterLines, key) {
5891
+ const pattern = new RegExp(`^${key}:\\s*(.+)\\s*$`, "i");
5892
+ for (const line of frontmatterLines) {
5893
+ const match = pattern.exec(line.trim());
5894
+ if (match?.[1]) {
5895
+ return stripYamlScalar2(match[1]);
5896
+ }
5897
+ }
5898
+ return void 0;
5899
+ }
5900
+ function parseCommandFrontmatterBoolean(frontmatterLines, key) {
5901
+ const value = parseCommandFrontmatterString(frontmatterLines, key);
5902
+ if (!value) return void 0;
5903
+ if (/^true$/i.test(value)) return true;
5904
+ if (/^false$/i.test(value)) return false;
5905
+ return void 0;
5906
+ }
5685
5907
  function walkMarkdownFiles2(dir) {
5686
5908
  const entries = readdirSync3(dir);
5687
5909
  const files = [];
@@ -5712,6 +5934,9 @@ function readCanonicalCommandFiles(commandsDir) {
5712
5934
  commandId,
5713
5935
  title,
5714
5936
  description: parseCommandFrontmatterDescription(frontmatterLines),
5937
+ agent: parseCommandFrontmatterString(frontmatterLines, "agent"),
5938
+ subtask: parseCommandFrontmatterBoolean(frontmatterLines, "subtask"),
5939
+ model: parseCommandFrontmatterString(frontmatterLines, "model"),
5715
5940
  body: body.trim()
5716
5941
  };
5717
5942
  });
@@ -5730,6 +5955,7 @@ var CodexGenerator = class extends Generator {
5730
5955
  async generate() {
5731
5956
  await Promise.all([
5732
5957
  this.generateManifest(),
5958
+ this.generateAppConfig(),
5733
5959
  this.generateMcpConfig(".mcp.json", {
5734
5960
  includeDefaultAuthHeaders: false,
5735
5961
  transformRemoteEntry: ({ name, server }) => {
@@ -5824,6 +6050,11 @@ var CodexGenerator = class extends Generator {
5824
6050
  }
5825
6051
  await this.writeJson(".codex-plugin/plugin.json", manifest);
5826
6052
  }
6053
+ async generateAppConfig() {
6054
+ const appConfig = this.config.platforms?.codex?.app;
6055
+ if (!appConfig || typeof appConfig !== "object" || Array.isArray(appConfig)) return;
6056
+ await this.writeJson(".app.json", appConfig);
6057
+ }
5827
6058
  async generatePermissionsCompanion() {
5828
6059
  const compilerIntent = this.getCompilerIntent();
5829
6060
  const rules = collectPermissionRules(this.config.permissions);
@@ -5961,8 +6192,8 @@ var CodexGenerator = class extends Generator {
5961
6192
  };
5962
6193
 
5963
6194
  // src/generators/opencode/index.ts
5964
- import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
5965
- import { resolve as resolve7 } from "path";
6195
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
6196
+ import { basename as basename3, resolve as resolve7 } from "path";
5966
6197
  var OpenCodeGenerator = class extends Generator {
5967
6198
  platform = "opencode";
5968
6199
  async generate() {
@@ -5972,9 +6203,11 @@ var OpenCodeGenerator = class extends Generator {
5972
6203
  ]);
5973
6204
  this.copySkills();
5974
6205
  this.copyCommands();
6206
+ this.copyAgents();
5975
6207
  this.copyScripts();
5976
6208
  this.copyAssets();
5977
6209
  this.copyPassthrough();
6210
+ this.rewriteOpenCodeSkillAgentMentions();
5978
6211
  }
5979
6212
  async generatePackageJson() {
5980
6213
  const npmName = this.config.platforms?.opencode?.npmPackage ?? `opencode-${this.config.name}`;
@@ -6286,7 +6519,10 @@ var OpenCodeGenerator = class extends Generator {
6286
6519
  for (const command2 of commands) {
6287
6520
  output[command2.commandId] = {
6288
6521
  template: command2.body,
6289
- ...command2.description ? { description: command2.description } : {}
6522
+ ...command2.description ? { description: command2.description } : {},
6523
+ ...command2.agent ? { agent: command2.agent } : {},
6524
+ ...typeof command2.subtask === "boolean" ? { subtask: command2.subtask } : {},
6525
+ ...command2.model ? { model: command2.model } : {}
6290
6526
  };
6291
6527
  }
6292
6528
  return output;
@@ -6312,14 +6548,36 @@ var OpenCodeGenerator = class extends Generator {
6312
6548
  if (typeof agent.frontmatter.temperature === "number") {
6313
6549
  definition.temperature = agent.frontmatter.temperature;
6314
6550
  }
6551
+ if (typeof agent.frontmatter.steps === "number") {
6552
+ definition.steps = agent.frontmatter.steps;
6553
+ }
6554
+ if (typeof agent.frontmatter.maxSteps === "number" && definition.steps === void 0) {
6555
+ definition.steps = agent.frontmatter.maxSteps;
6556
+ }
6557
+ if (typeof agent.frontmatter.disable === "boolean") {
6558
+ definition.disable = agent.frontmatter.disable;
6559
+ }
6315
6560
  if (typeof agent.frontmatter.hidden === "boolean") {
6316
6561
  definition.hidden = agent.frontmatter.hidden;
6317
6562
  }
6318
- const permission = asOpenCodeMap(agent.frontmatter.permission);
6563
+ if (typeof agent.frontmatter.color === "string" && agent.frontmatter.color) {
6564
+ definition.color = agent.frontmatter.color;
6565
+ }
6566
+ if (typeof agent.frontmatter.topP === "number") {
6567
+ definition.topP = agent.frontmatter.topP;
6568
+ }
6569
+ if (typeof agent.frontmatter.top_p === "number" && definition.topP === void 0) {
6570
+ definition.topP = agent.frontmatter.top_p;
6571
+ }
6572
+ const legacyToolTranslation = translateLegacyOpenCodeTools(agent.frontmatter.tools);
6573
+ const permission = mergeOpenCodeMaps(
6574
+ legacyToolTranslation.permission,
6575
+ asOpenCodeMap(agent.frontmatter.permission)
6576
+ );
6319
6577
  if (permission) {
6320
6578
  definition.permission = permission;
6321
6579
  }
6322
- const tools = asOpenCodeMap(agent.frontmatter.tools);
6580
+ const tools = legacyToolTranslation.untranslated;
6323
6581
  if (tools) {
6324
6582
  definition.tools = tools;
6325
6583
  }
@@ -6401,11 +6659,99 @@ var OpenCodeGenerator = class extends Generator {
6401
6659
  }
6402
6660
  return files;
6403
6661
  }
6662
+ rewriteOpenCodeSkillAgentMentions() {
6663
+ if (!this.config.agents || !this.config.skills) return;
6664
+ const skillsDir = resolve7(this.outDir, "skills");
6665
+ if (!existsSync11(skillsDir)) return;
6666
+ const agentsDir = this.resolveConfigPath(this.config.agents, "agents");
6667
+ const agentNames = readCanonicalAgentFiles(agentsDir).map((agent) => agent.name).filter(Boolean);
6668
+ if (agentNames.length === 0) return;
6669
+ for (const filePath of this.walkFiles(skillsDir)) {
6670
+ if (basename3(filePath) !== "SKILL.md") continue;
6671
+ const source = readFileSync5(filePath, "utf-8");
6672
+ let rewritten = source;
6673
+ for (const agentName of agentNames) {
6674
+ const escaped = escapeRegExp(agentName);
6675
+ rewritten = rewritten.replace(new RegExp(`\`(${escaped})\``, "g"), "`@$1`");
6676
+ }
6677
+ if (rewritten !== source) {
6678
+ writeFileSync2(filePath, rewritten);
6679
+ }
6680
+ }
6681
+ }
6404
6682
  };
6405
6683
  function asOpenCodeMap(value) {
6406
6684
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
6407
6685
  return value;
6408
6686
  }
6687
+ function mergeOpenCodeMaps(base, override) {
6688
+ if (!base) return override;
6689
+ if (!override) return base;
6690
+ const merged = { ...base };
6691
+ for (const [key, value] of Object.entries(override)) {
6692
+ if (isOpenCodeMap(merged[key]) && isOpenCodeMap(value)) {
6693
+ merged[key] = {
6694
+ ...merged[key],
6695
+ ...value
6696
+ };
6697
+ continue;
6698
+ }
6699
+ merged[key] = value;
6700
+ }
6701
+ return merged;
6702
+ }
6703
+ function translateLegacyOpenCodeTools(value) {
6704
+ const tools = asOpenCodeMap(value);
6705
+ if (!tools) return {};
6706
+ const permission = {};
6707
+ const untranslated = {};
6708
+ for (const [rawKey, rawValue] of Object.entries(tools)) {
6709
+ const key = normalizeLegacyOpenCodeToolKey(rawKey);
6710
+ const translated = translateLegacyOpenCodeToolValue(rawValue);
6711
+ if (translated === void 0) {
6712
+ untranslated[rawKey] = rawValue;
6713
+ continue;
6714
+ }
6715
+ permission[key] = translated;
6716
+ }
6717
+ return {
6718
+ ...Object.keys(permission).length > 0 ? { permission } : {},
6719
+ ...Object.keys(untranslated).length > 0 ? { untranslated } : {}
6720
+ };
6721
+ }
6722
+ function normalizeLegacyOpenCodeToolKey(key) {
6723
+ switch (key) {
6724
+ case "write":
6725
+ case "patch":
6726
+ case "multiedit":
6727
+ return "edit";
6728
+ case "shell":
6729
+ return "bash";
6730
+ default:
6731
+ return key;
6732
+ }
6733
+ }
6734
+ function translateLegacyOpenCodeToolValue(value) {
6735
+ if (typeof value === "boolean") {
6736
+ return value ? "allow" : "deny";
6737
+ }
6738
+ if (typeof value === "string" && ["allow", "ask", "deny"].includes(value)) {
6739
+ return value;
6740
+ }
6741
+ if (!isOpenCodeMap(value)) return void 0;
6742
+ const nested = {};
6743
+ for (const [key, rawNested] of Object.entries(value)) {
6744
+ const translated = translateLegacyOpenCodeToolValue(rawNested);
6745
+ if (translated === void 0 || typeof translated === "object") {
6746
+ return void 0;
6747
+ }
6748
+ nested[key] = translated;
6749
+ }
6750
+ return nested;
6751
+ }
6752
+ function isOpenCodeMap(value) {
6753
+ return !!value && typeof value === "object" && !Array.isArray(value);
6754
+ }
6409
6755
  function mapHookEventName(event) {
6410
6756
  const map = {
6411
6757
  sessionStart: "session.created",
@@ -6426,6 +6772,9 @@ function mapHookEventName(event) {
6426
6772
  function toPascalCase(str) {
6427
6773
  return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
6428
6774
  }
6775
+ function escapeRegExp(value) {
6776
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6777
+ }
6429
6778
 
6430
6779
  // src/generators/github-copilot/index.ts
6431
6780
  var GitHubCopilotGenerator = class extends Generator {
@@ -6673,6 +7022,40 @@ var GENERATORS = {
6673
7022
  cline: ClineGenerator,
6674
7023
  amp: AmpGenerator
6675
7024
  };
7025
+ function assertPathWithinRoot(rootDir, configPath, configKey) {
7026
+ const resolvedPath = resolve8(rootDir, configPath);
7027
+ const rel = relative5(rootDir, resolvedPath);
7028
+ if (rel.startsWith("..")) {
7029
+ throw new Error(`${configKey} path "${configPath}" resolves outside the project root.`);
7030
+ }
7031
+ }
7032
+ function validateConfiguredPaths(config, rootDir) {
7033
+ assertPathWithinRoot(rootDir, config.skills, "skills");
7034
+ if (config.commands) {
7035
+ assertPathWithinRoot(rootDir, config.commands, "commands");
7036
+ }
7037
+ if (config.agents) {
7038
+ assertPathWithinRoot(rootDir, config.agents, "agents");
7039
+ }
7040
+ if (config.scripts) {
7041
+ assertPathWithinRoot(rootDir, config.scripts, "scripts");
7042
+ }
7043
+ if (config.assets) {
7044
+ assertPathWithinRoot(rootDir, config.assets, "assets");
7045
+ }
7046
+ if (config.instructions) {
7047
+ assertPathWithinRoot(rootDir, config.instructions, "instructions");
7048
+ }
7049
+ for (const passthroughPath of config.passthrough ?? []) {
7050
+ assertPathWithinRoot(rootDir, passthroughPath, "passthrough");
7051
+ }
7052
+ if (config.brand?.icon) {
7053
+ assertPathWithinRoot(rootDir, config.brand.icon, "brand.icon");
7054
+ }
7055
+ for (const screenshot of config.brand?.screenshots ?? []) {
7056
+ assertPathWithinRoot(rootDir, screenshot, "brand.screenshots");
7057
+ }
7058
+ }
6676
7059
  async function build(config, rootDir, options = {}) {
6677
7060
  const targets = options.targets ?? config.targets;
6678
7061
  const outDir = resolve8(rootDir, config.outDir);
@@ -6682,10 +7065,11 @@ async function build(config, rootDir, options = {}) {
6682
7065
  `outDir "${config.outDir}" resolves outside the project root. Refusing to delete.`
6683
7066
  );
6684
7067
  }
7068
+ validateConfiguredPaths(config, rootDir);
6685
7069
  if (options.clean !== false) {
6686
7070
  rmSync(outDir, { recursive: true, force: true });
6687
7071
  }
6688
- mkdirSync2(outDir, { recursive: true });
7072
+ mkdirSync3(outDir, { recursive: true });
6689
7073
  const generators = targets.map((target) => {
6690
7074
  const GeneratorClass = GENERATORS[target];
6691
7075
  if (!GeneratorClass) {
@@ -6705,7 +7089,7 @@ import { spawn as spawn2 } from "child_process";
6705
7089
 
6706
7090
  // src/cli/lint.ts
6707
7091
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "fs";
6708
- import { resolve as resolve9, relative as relative6, basename as basename3, dirname as dirname3 } from "path";
7092
+ import { resolve as resolve9, relative as relative6, basename as basename4, dirname as dirname3 } from "path";
6709
7093
 
6710
7094
  // src/validation/platform-rules.ts
6711
7095
  var STANDARD_SKILL_FRONTMATTER = [
@@ -6813,10 +7197,22 @@ var PLATFORM_LIMIT_POLICIES = {
6813
7197
  },
6814
7198
  "codex": {
6815
7199
  ...NULL_LIMIT_POLICIES,
6816
- skillDescriptionMax: { kind: "hard" },
6817
- skillNameMustMatchDir: { kind: "hard" },
6818
- manifestPromptMax: { kind: "hard" },
6819
- manifestPromptCountMax: { kind: "hard" },
7200
+ skillDescriptionMax: {
7201
+ kind: "advisory",
7202
+ notes: "Pluxx keeps Codex descriptions concise at 1,024 characters as a conservative compatibility heuristic; the current docs do not state this as an official hard cap."
7203
+ },
7204
+ skillNameMustMatchDir: {
7205
+ kind: "advisory",
7206
+ notes: "Pluxx keeps Codex skill directory names aligned with skill names for portability and predictability, but the current docs do not state this as a formal hard requirement."
7207
+ },
7208
+ manifestPromptMax: {
7209
+ kind: "advisory",
7210
+ notes: "Pluxx keeps Codex default prompts short at 128 characters as a conservative listing heuristic; the current docs do not publish this as a hard limit."
7211
+ },
7212
+ manifestPromptCountMax: {
7213
+ kind: "advisory",
7214
+ notes: "Pluxx keeps Codex default prompt count to three as a conservative listing heuristic; the current docs do not publish this as a hard limit."
7215
+ },
6820
7216
  manifestPathPrefix: { kind: "hard" },
6821
7217
  instructionsMaxBytes: {
6822
7218
  kind: "hard",
@@ -6869,7 +7265,7 @@ var PLATFORM_LIMIT_POLICIES = {
6869
7265
  var PLATFORM_VALIDATION_RULES = {
6870
7266
  "claude-code": {
6871
7267
  platform: "claude-code",
6872
- summary: "Claude Code plugins use an optional manifest at .claude-plugin/plugin.json with auto-discovery for skills, commands, agents, hooks, MCP, and output styles.",
7268
+ summary: "Claude Code plugins use an optional manifest at .claude-plugin/plugin.json with auto-discovery for skills, commands, agents, hooks, MCP, marketplaces, and output styles.",
6873
7269
  limits: PLATFORM_LIMITS["claude-code"],
6874
7270
  limitPolicies: PLATFORM_LIMIT_POLICIES["claude-code"],
6875
7271
  skillDiscoveryDirs: [
@@ -6877,7 +7273,21 @@ var PLATFORM_VALIDATION_RULES = {
6877
7273
  ],
6878
7274
  frontmatter: {
6879
7275
  standard: [...STANDARD_SKILL_FRONTMATTER],
6880
- additional: []
7276
+ additional: [
7277
+ "when_to_use",
7278
+ "argument-hint",
7279
+ "arguments",
7280
+ "user-invocable",
7281
+ "allowed-tools",
7282
+ "model",
7283
+ "effort",
7284
+ "context",
7285
+ "agent",
7286
+ "hooks",
7287
+ "paths",
7288
+ "shell"
7289
+ ],
7290
+ notes: "Claude exposes the richest documented skill frontmatter of the core four."
6881
7291
  },
6882
7292
  manifest: {
6883
7293
  files: [".claude-plugin/plugin.json"],
@@ -6885,43 +7295,58 @@ var PLATFORM_VALIDATION_RULES = {
6885
7295
  notes: "The manifest is optional; if present, name is the only required field."
6886
7296
  },
6887
7297
  mcp: {
6888
- files: [".mcp.json"],
7298
+ files: [".mcp.json", ".claude-plugin/plugin.json"],
6889
7299
  rootKey: "mcpServers",
6890
7300
  transports: ["stdio", "http", "sse"],
6891
- auth: ["headers", "env interpolation"],
6892
- notes: "Claude Code supports either inline MCP config in plugin.json or a separate .mcp.json file."
7301
+ auth: ["headers", "env interpolation", "OAuth 2.0", "bearer tokens", "dynamic headers"],
7302
+ notes: "Claude Code supports either inline MCP config in plugin.json or a separate .mcp.json file, with marketplace and dependency-aware install flows."
6893
7303
  },
6894
7304
  hooks: {
6895
7305
  supported: true,
6896
- files: ["hooks/hooks.json"],
6897
- eventNames: [],
6898
- notes: "Hook configs can be stored in hooks/hooks.json or inlined in plugin.json."
7306
+ files: ["hooks/hooks.json", ".claude-plugin/plugin.json", "~/.claude/settings.json", ".claude/settings.json", ".claude/settings.local.json"],
7307
+ eventNames: ["SessionStart", "PreToolUse", "PostToolUse", "PermissionRequest", "TaskCreated", "TaskCompleted", "Stop", "Notification", "ConfigChange"],
7308
+ notes: "Hook configs can be stored in hooks/hooks.json, inlined in plugin.json, added in settings files, or scoped through skill and agent frontmatter."
6899
7309
  },
6900
7310
  instructions: {
6901
7311
  files: ["CLAUDE.md"],
6902
- format: "markdown"
7312
+ format: "markdown",
7313
+ notes: "Claude keeps persistent instructions in CLAUDE.md and pushes longer procedures into skills."
6903
7314
  },
6904
7315
  sources: [
6905
- { label: "Claude Code headless docs", url: "https://code.claude.com/docs/en/headless" },
7316
+ { label: "Claude Code MCP docs", url: "https://code.claude.com/docs/en/mcp" },
7317
+ { label: "Claude Code plugin marketplaces docs", url: "https://code.claude.com/docs/en/plugin-marketplaces" },
7318
+ { label: "Claude Code plugin dependencies docs", url: "https://code.claude.com/docs/en/plugin-dependencies" },
7319
+ { label: "Claude Code features overview", url: "https://code.claude.com/docs/en/features-overview" },
7320
+ { label: "Claude Code best practices", url: "https://code.claude.com/docs/en/best-practices" },
6906
7321
  { label: "Claude Code CLI reference", url: "https://code.claude.com/docs/en/cli-reference" },
6907
7322
  { label: "Claude Code discover plugins docs", url: "https://code.claude.com/docs/en/discover-plugins" },
7323
+ { label: "Claude Code plugins docs", url: "https://code.claude.com/docs/en/plugins" },
6908
7324
  { label: "Claude Code plugins reference", url: "https://code.claude.com/docs/en/plugins-reference" },
7325
+ { label: "Claude Code hooks guide", url: "https://code.claude.com/docs/en/hooks-guide" },
6909
7326
  { label: "Claude Code hooks docs", url: "https://code.claude.com/docs/en/hooks" },
6910
- { label: "Claude Code skills docs", url: "https://code.claude.com/docs/en/skills" }
7327
+ { label: "Claude Code skills docs", url: "https://code.claude.com/docs/en/skills" },
7328
+ { label: "Claude Code sub-agents docs", url: "https://code.claude.com/docs/en/sub-agents" },
7329
+ { label: "Claude Code env vars docs", url: "https://code.claude.com/docs/en/env-vars" }
6911
7330
  ]
6912
7331
  },
6913
7332
  "cursor": {
6914
7333
  platform: "cursor",
6915
- summary: "Cursor plugins use .cursor-plugin/plugin.json plus auto-discovered rules, skills, agents, commands, hooks, and mcp.json at the plugin root; Cursor subagents are a related but separate surface under .cursor/agents and ~/.cursor/agents.",
7334
+ summary: "Cursor plugins use .cursor-plugin/plugin.json plus native rules, skills, hooks, MCP, marketplace metadata, and subagent surfaces, with additional project and user config outside the plugin bundle.",
6916
7335
  limits: PLATFORM_LIMITS["cursor"],
6917
7336
  limitPolicies: PLATFORM_LIMIT_POLICIES["cursor"],
6918
7337
  skillDiscoveryDirs: [
6919
7338
  { path: "skills/", level: "supported" },
6920
- { path: "SKILL.md", level: "fallback", notes: "Used when no skills directory or manifest skill path is present." }
7339
+ { path: ".cursor/skills/", level: "supported" },
7340
+ { path: "~/.cursor/skills/", level: "supported" },
7341
+ { path: ".agents/skills/", level: "supported" },
7342
+ { path: "~/.agents/skills/", level: "supported" },
7343
+ { path: ".claude/skills/", level: "supported", notes: "Compatibility directory" },
7344
+ { path: ".codex/skills/", level: "supported", notes: "Compatibility directory" }
6921
7345
  ],
6922
7346
  frontmatter: {
6923
7347
  standard: [...STANDARD_SKILL_FRONTMATTER],
6924
- additional: []
7348
+ additional: [],
7349
+ notes: "Cursor skills document the shared frontmatter set plus compatibility metadata and supporting-file patterns."
6925
7350
  },
6926
7351
  manifest: {
6927
7352
  files: [".cursor-plugin/plugin.json"],
@@ -6929,16 +7354,16 @@ var PLATFORM_VALIDATION_RULES = {
6929
7354
  notes: "Cursor documents plugin.json as the required plugin manifest."
6930
7355
  },
6931
7356
  mcp: {
6932
- files: ["mcp.json"],
7357
+ files: ["mcp.json", ".cursor/mcp.json", "~/.cursor/mcp.json"],
6933
7358
  rootKey: "mcpServers",
6934
- transports: ["stdio", "http", "sse"],
6935
- auth: ["headers", "env interpolation"]
7359
+ transports: ["stdio", "sse", "streamable-http"],
7360
+ auth: ["headers", "env interpolation", "OAuth", "static OAuth credentials"]
6936
7361
  },
6937
7362
  hooks: {
6938
7363
  supported: true,
6939
- files: ["hooks/hooks.json"],
6940
- eventNames: [],
6941
- notes: "Cursor plugin hooks live under hooks/hooks.json; project hooks also exist separately in .cursor/hooks.json."
7364
+ files: ["hooks/hooks.json", ".cursor/hooks.json", "~/.cursor/hooks.json"],
7365
+ eventNames: ["sessionStart", "preToolUse", "postToolUse", "subagentStart", "subagentStop", "beforeShellExecution", "afterShellExecution"],
7366
+ notes: "Cursor plugin hooks live under hooks/hooks.json; project and user hooks also exist separately and reload on save."
6942
7367
  },
6943
7368
  instructions: {
6944
7369
  files: ["rules/", "AGENTS.md"],
@@ -6946,25 +7371,32 @@ var PLATFORM_VALIDATION_RULES = {
6946
7371
  notes: "rules/ is the plugin-native instruction surface. AGENTS.md remains useful as shared repo guidance. Cursor subagents use markdown files under .cursor/agents or ~/.cursor/agents (with .claude/.codex compatibility paths)."
6947
7372
  },
6948
7373
  sources: [
6949
- { label: "Cursor plugins reference", url: "https://cursor.com/docs/reference/plugins" },
6950
7374
  { label: "Cursor plugins overview", url: "https://cursor.com/docs/plugins" },
6951
7375
  { label: "Cursor hooks docs", url: "https://cursor.com/docs/hooks" },
6952
7376
  { label: "Cursor skills docs", url: "https://cursor.com/docs/skills" },
6953
7377
  { label: "Cursor rules docs", url: "https://cursor.com/docs/rules" },
6954
7378
  { label: "Cursor MCP docs", url: "https://cursor.com/docs/mcp" },
6955
7379
  { label: "Cursor CLI headless docs", url: "https://cursor.com/docs/cli/headless" },
7380
+ { label: "Cursor CLI slash commands", url: "https://cursor.com/docs/cli/reference/slash-commands" },
6956
7381
  { label: "Cursor CLI parameters", url: "https://cursor.com/docs/cli/reference/parameters" },
6957
7382
  { label: "Cursor CLI authentication", url: "https://cursor.com/docs/cli/reference/authentication" },
7383
+ { label: "Cursor CLI permissions", url: "https://cursor.com/docs/cli/reference/permissions" },
7384
+ { label: "Cursor CLI configuration", url: "https://cursor.com/docs/cli/reference/configuration" },
7385
+ { label: "Cursor ACP docs", url: "https://cursor.com/docs/cli/acp" },
6958
7386
  { label: "Cursor subagents docs", url: "https://cursor.com/docs/subagents" }
6959
7387
  ]
6960
7388
  },
6961
7389
  "codex": {
6962
7390
  platform: "codex",
6963
- summary: "Codex plugins use .codex-plugin/plugin.json with skills, .mcp.json, optional app mappings, and AGENTS.md; current docs separate plugin packaging from hooks configuration and do not document plugin-provided slash commands.",
7391
+ summary: "Codex plugins use .codex-plugin/plugin.json with skills, optional .mcp.json and .app.json, marketplace catalogs, cache installs, AGENTS.md instructions, and separate hook configuration.",
6964
7392
  limits: PLATFORM_LIMITS["codex"],
6965
7393
  limitPolicies: PLATFORM_LIMIT_POLICIES["codex"],
6966
7394
  skillDiscoveryDirs: [
6967
- { path: "skills/", level: "supported" }
7395
+ { path: "skills/", level: "supported" },
7396
+ { path: "$CWD/.agents/skills/", level: "supported" },
7397
+ { path: "ancestor .agents/skills/", level: "supported", notes: "Walks upward until repo root" },
7398
+ { path: "$HOME/.agents/skills/", level: "supported" },
7399
+ { path: "/etc/codex/skills/", level: "supported" }
6968
7400
  ],
6969
7401
  frontmatter: {
6970
7402
  standard: [...STANDARD_SKILL_FRONTMATTER],
@@ -6976,68 +7408,95 @@ var PLATFORM_VALIDATION_RULES = {
6976
7408
  notes: "The build plugins guide documents plugin.json, skills/, .mcp.json, .app.json, and assets/ as the standard plugin structure."
6977
7409
  },
6978
7410
  mcp: {
6979
- files: [".mcp.json"],
7411
+ files: [".mcp.json", ".codex/config.toml"],
6980
7412
  rootKey: "mcpServers",
6981
- transports: ["stdio", "http", "sse"],
6982
- auth: ["bearer_token_env_var", "env_http_headers", "http_headers", "platform-managed auth"],
6983
- notes: "The current build guide documents mcpServers as a path to .mcp.json in the plugin bundle."
7413
+ transports: ["stdio", "streamable-http"],
7414
+ auth: ["bearer token", "OAuth", "header env vars"],
7415
+ notes: "The current build guide documents mcpServers as a path to .mcp.json in the plugin bundle, while active MCP state also lives in config.toml."
6984
7416
  },
6985
7417
  hooks: {
6986
7418
  supported: true,
6987
7419
  files: [".codex/hooks.json", "~/.codex/hooks.json"],
6988
- eventNames: [],
6989
- notes: "Codex documents hooks in project/user config, but the current plugin build guide does not document plugin-packaged hooks."
7420
+ eventNames: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
7421
+ notes: "Codex documents hooks in project/user config, guarded by the codex_hooks feature flag; the current plugin build guide does not document plugin-packaged hooks."
6990
7422
  },
6991
7423
  instructions: {
6992
- files: ["AGENTS.md"],
6993
- format: "markdown"
7424
+ files: ["AGENTS.md", "AGENTS.override.md"],
7425
+ format: "markdown",
7426
+ notes: "Codex also supports model instruction overrides plus configurable fallback filenames for project docs."
6994
7427
  },
6995
7428
  sources: [
7429
+ { label: "Codex plugins docs", url: "https://developers.openai.com/codex/plugins" },
6996
7430
  { label: "Codex build plugins docs", url: "https://developers.openai.com/codex/plugins/build" },
7431
+ { label: "Codex CLI features docs", url: "https://developers.openai.com/codex/cli/features" },
7432
+ { label: "Codex CLI reference docs", url: "https://developers.openai.com/codex/cli/reference" },
7433
+ { label: "Codex slash commands docs", url: "https://developers.openai.com/codex/cli/slash-commands" },
7434
+ { label: "Codex advanced config docs", url: "https://developers.openai.com/codex/config-advanced" },
7435
+ { label: "Codex rules docs", url: "https://developers.openai.com/codex/rules" },
6997
7436
  { label: "Codex hooks docs", url: "https://developers.openai.com/codex/hooks" },
6998
7437
  { label: "Codex skills docs", url: "https://developers.openai.com/codex/skills" },
6999
7438
  { label: "Codex MCP docs", url: "https://developers.openai.com/codex/mcp" },
7000
- { label: "Codex AGENTS.md guide", url: "https://developers.openai.com/codex/guides/agents-md" }
7439
+ { label: "Codex AGENTS.md guide", url: "https://developers.openai.com/codex/guides/agents-md" },
7440
+ { label: "Codex subagents docs", url: "https://developers.openai.com/codex/subagents" },
7441
+ { label: "Codex subagents concept docs", url: "https://developers.openai.com/codex/concepts/subagents" },
7442
+ { label: "Codex noninteractive docs", url: "https://developers.openai.com/codex/noninteractive" },
7443
+ { label: "Codex SDK docs", url: "https://developers.openai.com/codex/sdk" },
7444
+ { label: "Codex agents SDK guide", url: "https://developers.openai.com/codex/guides/agents-sdk" }
7001
7445
  ]
7002
7446
  },
7003
7447
  "opencode": {
7004
7448
  platform: "opencode",
7005
- summary: "OpenCode plugins are code-first TypeScript or JavaScript modules that register skills, commands, MCP servers, and hook handlers programmatically.",
7449
+ summary: "OpenCode plugins are code-first JS or TS modules loaded from local plugin dirs or npm references in config, with native skills, commands, agents, MCP, and permission surfaces.",
7006
7450
  limits: PLATFORM_LIMITS["opencode"],
7007
7451
  limitPolicies: PLATFORM_LIMIT_POLICIES["opencode"],
7008
7452
  skillDiscoveryDirs: [
7009
- { path: "skills/", level: "supported" }
7453
+ { path: "skills/", level: "supported" },
7454
+ { path: ".opencode/skills/", level: "supported" },
7455
+ { path: "~/.config/opencode/skills/", level: "supported" },
7456
+ { path: ".claude/skills/", level: "supported", notes: "Compatibility directory" },
7457
+ { path: ".agents/skills/", level: "supported", notes: "Compatibility directory" }
7010
7458
  ],
7011
7459
  frontmatter: {
7012
7460
  standard: [...STANDARD_SKILL_FRONTMATTER],
7013
- additional: []
7461
+ additional: [],
7462
+ notes: "OpenCode supports Agent Skills semantics, but plugin runtime behavior is code-first rather than manifest-first."
7014
7463
  },
7015
7464
  manifest: {
7016
- files: ["package.json", "index.ts"],
7017
- required: true,
7018
- notes: "OpenCode plugins are loaded as local modules or npm packages rather than a JSON manifest-only bundle."
7465
+ files: ["opencode.json", ".opencode/plugins/", "~/.config/opencode/plugins/"],
7466
+ required: false,
7467
+ notes: "OpenCode plugins are loaded as local modules or npm packages through config rather than a dedicated manifest-only bundle."
7019
7468
  },
7020
7469
  mcp: {
7021
- files: ["index.ts"],
7470
+ files: ["opencode.json"],
7471
+ rootKey: "mcp",
7022
7472
  transports: ["local", "remote"],
7023
- auth: ["headers", "programmatic env interpolation", "OAuth"],
7024
- notes: 'OpenCode plugin code mutates Config["mcp"] programmatically; the underlying platform config supports local and remote servers.'
7473
+ auth: ["headers", "env interpolation", "OAuth"],
7474
+ notes: "OpenCode config owns MCP; plugins can also extend runtime behavior programmatically."
7025
7475
  },
7026
7476
  hooks: {
7027
7477
  supported: true,
7028
- files: ["index.ts"],
7478
+ files: ["plugin module (index.ts/index.js)"],
7029
7479
  eventNames: [],
7030
7480
  notes: "OpenCode hooks are plugin event handlers implemented in code, not a separate hooks.json file."
7031
7481
  },
7032
7482
  instructions: {
7033
- files: ["index.ts"],
7034
- format: "typescript",
7035
- notes: "Plugins inject instructions into the runtime system prompt from code."
7483
+ files: ["AGENTS.md", "CLAUDE.md", "opencode.json"],
7484
+ format: "markdown + json + code",
7485
+ notes: "OpenCode supports AGENTS.md, CLAUDE.md fallback, config instructions, and plugin runtime instruction injection."
7036
7486
  },
7037
7487
  sources: [
7488
+ { label: "OpenCode SDK docs", url: "https://opencode.ai/docs/sdk/" },
7489
+ { label: "OpenCode server docs", url: "https://opencode.ai/docs/server/" },
7490
+ { label: "OpenCode config docs", url: "https://opencode.ai/docs/config/" },
7038
7491
  { label: "OpenCode plugins docs", url: "https://opencode.ai/docs/plugins/" },
7039
7492
  { label: "OpenCode skills docs", url: "https://opencode.ai/docs/skills/" },
7040
- { label: "OpenCode MCP servers docs", url: "https://opencode.ai/docs/mcp-servers/" }
7493
+ { label: "OpenCode commands docs", url: "https://opencode.ai/docs/commands/" },
7494
+ { label: "OpenCode agents docs", url: "https://opencode.ai/docs/agents/" },
7495
+ { label: "OpenCode MCP servers docs", url: "https://opencode.ai/docs/mcp-servers/" },
7496
+ { label: "OpenCode custom tools docs", url: "https://opencode.ai/docs/custom-tools/" },
7497
+ { label: "OpenCode permissions docs", url: "https://opencode.ai/docs/permissions/" },
7498
+ { label: "OpenCode rules docs", url: "https://opencode.ai/docs/rules/" },
7499
+ { label: "OpenCode ACP docs", url: "https://opencode.ai/docs/acp/" }
7041
7500
  ]
7042
7501
  },
7043
7502
  "openhands": {
@@ -7301,7 +7760,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7301
7760
  },
7302
7761
  commands: {
7303
7762
  mode: "preserve",
7304
- nativeSurfaces: ["commands/*.md"]
7763
+ nativeSurfaces: ["commands/*.md", "skills/<skill>/SKILL.md"],
7764
+ notes: "Claude still supports command files, but the product is increasingly converging command workflows into skills."
7305
7765
  },
7306
7766
  agents: {
7307
7767
  mode: "preserve",
@@ -7310,7 +7770,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7310
7770
  },
7311
7771
  hooks: {
7312
7772
  mode: "preserve",
7313
- nativeSurfaces: ["hooks/hooks.json", ".claude-plugin/plugin.json"]
7773
+ nativeSurfaces: ["hooks/hooks.json", ".claude-plugin/plugin.json", "settings hooks", "skill/agent frontmatter hooks"]
7314
7774
  },
7315
7775
  permissions: {
7316
7776
  mode: "translate",
@@ -7323,8 +7783,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7323
7783
  },
7324
7784
  distribution: {
7325
7785
  mode: "translate",
7326
- nativeSurfaces: [".claude-plugin/plugin.json", "install scopes", "user configuration"],
7327
- notes: "Distribution surfaces are native, but shared brand metadata is narrower than Codex."
7786
+ nativeSurfaces: [".claude-plugin/plugin.json", "marketplaces", "install scopes", "user configuration", "/reload-plugins"],
7787
+ notes: "Distribution surfaces are native, including plugin marketplaces and explicit reload behavior."
7328
7788
  }
7329
7789
  },
7330
7790
  sources: PLATFORM_VALIDATION_RULES["claude-code"].sources
@@ -7343,7 +7803,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7343
7803
  },
7344
7804
  commands: {
7345
7805
  mode: "preserve",
7346
- nativeSurfaces: ["commands/*"]
7806
+ nativeSurfaces: ["commands/*", "slash commands"]
7347
7807
  },
7348
7808
  agents: {
7349
7809
  mode: "translate",
@@ -7361,11 +7821,11 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7361
7821
  },
7362
7822
  runtime: {
7363
7823
  mode: "preserve",
7364
- nativeSurfaces: ["mcp.json", ".cursor-plugin/plugin.json", "scripts/", "assets/"]
7824
+ nativeSurfaces: ["mcp.json", ".cursor/mcp.json", "~/.cursor/mcp.json", ".cursor-plugin/plugin.json", "scripts/", "assets/"]
7365
7825
  },
7366
7826
  distribution: {
7367
7827
  mode: "preserve",
7368
- nativeSurfaces: [".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json", "local marketplace install path"]
7828
+ nativeSurfaces: [".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json", "local marketplace install path", "reload window / restart"]
7369
7829
  }
7370
7830
  },
7371
7831
  sources: PLATFORM_VALIDATION_RULES["cursor"].sources
@@ -7408,7 +7868,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7408
7868
  },
7409
7869
  distribution: {
7410
7870
  mode: "preserve",
7411
- nativeSurfaces: [".codex-plugin/plugin.json", "~/.agents/plugins/marketplace.json", "$REPO_ROOT/.agents/plugins/marketplace.json"]
7871
+ nativeSurfaces: [".codex-plugin/plugin.json", "~/.agents/plugins/marketplace.json", "$REPO_ROOT/.agents/plugins/marketplace.json", "cache install path", "restart after update"]
7412
7872
  }
7413
7873
  },
7414
7874
  sources: PLATFORM_VALIDATION_RULES["codex"].sources
@@ -7418,7 +7878,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7418
7878
  buckets: {
7419
7879
  instructions: {
7420
7880
  mode: "translate",
7421
- nativeSurfaces: ["config instructions", "plugin code"],
7881
+ nativeSurfaces: ["AGENTS.md", "CLAUDE.md", "config instructions", "plugin code"],
7422
7882
  notes: "OpenCode instructions are native, but the surface is config- and code-driven rather than manifest markdown only."
7423
7883
  },
7424
7884
  skills: {
@@ -7431,7 +7891,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7431
7891
  },
7432
7892
  agents: {
7433
7893
  mode: "preserve",
7434
- nativeSurfaces: ["agents/*.md", "config agent definitions"]
7894
+ nativeSurfaces: ["agents/*.md", "config agent definitions"],
7895
+ notes: "OpenCode agents are first-class native surfaces. Prefer permission-first agent config for new builds; legacy tools remains compatibility input, not the preferred emitted shape."
7435
7896
  },
7436
7897
  hooks: {
7437
7898
  mode: "translate",
@@ -7440,21 +7901,25 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7440
7901
  },
7441
7902
  permissions: {
7442
7903
  mode: "preserve",
7443
- nativeSurfaces: ["config permission", "per-agent overrides"]
7904
+ nativeSurfaces: ["config permission", "per-agent overrides"],
7905
+ notes: "OpenCode permission is keyed by tool name and patterns, including native skill and task controls. Legacy tools booleans are deprecated in favor of permission."
7444
7906
  },
7445
7907
  runtime: {
7446
7908
  mode: "preserve",
7447
- nativeSurfaces: ["config mcp", "plugin JS/TS runtime", "scripts/", "assets/"]
7909
+ nativeSurfaces: ["opencode.json", "config mcp", "plugin JS/TS runtime", "scripts/", "assets/"]
7448
7910
  },
7449
7911
  distribution: {
7450
7912
  mode: "translate",
7451
- nativeSurfaces: ["local plugin dir", "npm package", "plugin JS/TS entrypoint"],
7913
+ nativeSurfaces: [".opencode/plugins/", "~/.config/opencode/plugins/", "opencode.json", "npm package", "plugin JS/TS entrypoint"],
7452
7914
  notes: "Distribution is native, but there is no single shared manifest analog to Claude, Cursor, or Codex."
7453
7915
  }
7454
7916
  },
7455
7917
  sources: PLATFORM_VALIDATION_RULES["opencode"].sources
7456
7918
  }
7457
7919
  };
7920
+ function getPlatformRules(platform) {
7921
+ return PLATFORM_VALIDATION_RULES[platform];
7922
+ }
7458
7923
  function getCoreFourPrimitiveCapabilities(platform) {
7459
7924
  return CORE_FOUR_PRIMITIVE_CAPABILITIES[platform];
7460
7925
  }
@@ -7520,6 +7985,21 @@ function renderPrimitiveTranslationSummary(summary) {
7520
7985
  lines.push(` ${row.bucket.padEnd(bucketWidth, " ")} ${cells.join(" ")}`);
7521
7986
  }
7522
7987
  lines.push(" legend: keep=preserve xlat=translate weak=degrade drop=drop");
7988
+ const detailLines = [];
7989
+ for (const row of summary.rows) {
7990
+ for (const target of summary.targets) {
7991
+ const mode = row.modes[target];
7992
+ if (!mode || mode === "preserve") continue;
7993
+ const capability = getCoreFourPrimitiveCapabilities(target).buckets[row.bucket];
7994
+ const verb = mode === "translate" ? "re-expressed via" : mode === "degrade" ? "weakened to" : "omitted; nearest surface would be";
7995
+ const suffix = capability.notes ? ` ${capability.notes}` : "";
7996
+ detailLines.push(` - ${row.bucket} on ${TARGET_LABELS[target]}: ${verb} ${capability.nativeSurfaces.join(", ")}.${suffix}`);
7997
+ }
7998
+ }
7999
+ if (detailLines.length > 0) {
8000
+ lines.push(" details:");
8001
+ lines.push(...detailLines);
8002
+ }
7523
8003
  return lines;
7524
8004
  }
7525
8005
 
@@ -7710,16 +8190,27 @@ function lintSkillFile(skillFile, targets, issues, frontmatterCache) {
7710
8190
  platform: "Agent Skills"
7711
8191
  });
7712
8192
  }
7713
- const expectedDirName = basename3(dirname3(skillFile));
8193
+ const expectedDirName = basename4(dirname3(skillFile));
7714
8194
  const platformsRequiringDirMatch = targets.filter((t2) => PLATFORM_LIMITS[t2].skillNameMustMatchDir);
7715
- if (platformsRequiringDirMatch.length > 0 && nameField.value !== expectedDirName) {
7716
- const platformNames = platformsRequiringDirMatch.join(", ");
8195
+ const hardDirMatchPlatforms = platformsRequiringDirMatch.filter((target) => PLATFORM_LIMIT_POLICIES[target].skillNameMustMatchDir.kind === "hard");
8196
+ const advisoryDirMatchPlatforms = platformsRequiringDirMatch.filter((target) => PLATFORM_LIMIT_POLICIES[target].skillNameMustMatchDir.kind !== "hard");
8197
+ if (hardDirMatchPlatforms.length > 0 && nameField.value !== expectedDirName) {
8198
+ const platformNames = hardDirMatchPlatforms.join(", ");
7717
8199
  pushIssue(issues, {
7718
8200
  level: "error",
7719
8201
  code: "skill-name-dir-mismatch",
7720
8202
  message: `Skill name "${nameField.value}" must match directory name "${expectedDirName}" (required by ${platformNames}).`,
7721
8203
  file: skillFile,
7722
- platform: platformsRequiringDirMatch[0]
8204
+ platform: hardDirMatchPlatforms[0]
8205
+ });
8206
+ } else if (advisoryDirMatchPlatforms.length > 0 && nameField.value !== expectedDirName) {
8207
+ const platformNames = advisoryDirMatchPlatforms.join(", ");
8208
+ pushIssue(issues, {
8209
+ level: "warning",
8210
+ code: "skill-name-dir-guideline",
8211
+ message: `Skill name "${nameField.value}" should match directory name "${expectedDirName}" for ${platformNames} compatibility.`,
8212
+ file: skillFile,
8213
+ platform: advisoryDirMatchPlatforms[0]
7723
8214
  });
7724
8215
  }
7725
8216
  if (!nameField.quoted && needsQuotes(nameField.rawValue)) {
@@ -7744,10 +8235,11 @@ function lintSkillFile(skillFile, targets, issues, frontmatterCache) {
7744
8235
  for (const target of targets) {
7745
8236
  const limits = PLATFORM_LIMITS[target];
7746
8237
  if (limits.skillDescriptionMax !== null && descriptionField.value.length > limits.skillDescriptionMax) {
8238
+ const policy = PLATFORM_LIMIT_POLICIES[target].skillDescriptionMax;
7747
8239
  pushIssue(issues, {
7748
- level: "error",
7749
- code: "skill-description-length",
7750
- message: `Description exceeds ${target} max of ${limits.skillDescriptionMax} characters.`,
8240
+ level: policy?.kind === "hard" ? "error" : "warning",
8241
+ code: policy?.kind === "hard" ? "skill-description-length" : "skill-description-guideline",
8242
+ message: policy?.kind === "hard" ? `Description exceeds ${target} max of ${limits.skillDescriptionMax} characters.` : `Description exceeds the Pluxx ${target} compatibility guideline of ${limits.skillDescriptionMax} characters.`,
7751
8243
  file: skillFile,
7752
8244
  platform: target
7753
8245
  });
@@ -7946,11 +8438,47 @@ function lintMcpUrls(config, issues) {
7946
8438
  platform: "MCP"
7947
8439
  });
7948
8440
  }
7949
- } catch {
8441
+ } catch {
8442
+ pushIssue(issues, {
8443
+ level: "error",
8444
+ code: "mcp-url-invalid",
8445
+ message: `MCP server "${serverName}" has an invalid URL.`,
8446
+ file: "pluxx.config.ts",
8447
+ platform: "MCP"
8448
+ });
8449
+ }
8450
+ }
8451
+ }
8452
+ function lintMcpRuntimeState(config, issues) {
8453
+ if (!config.mcp) return;
8454
+ const claudeUsesPlatformAuth = config.targets.includes("claude-code") && config.platforms?.["claude-code"]?.mcpAuth === "platform";
8455
+ const cursorUsesPlatformAuth = config.targets.includes("cursor") && config.platforms?.cursor?.mcpAuth === "platform";
8456
+ for (const [serverName, server] of Object.entries(config.mcp)) {
8457
+ if (server.transport === "stdio") {
8458
+ pushIssue(issues, {
8459
+ level: "warning",
8460
+ code: "mcp-stdio-runtime-dependency",
8461
+ message: `MCP server "${serverName}" runs through a local stdio command. End users still need that command and its runtime dependencies available after install.`,
8462
+ file: "pluxx.config.ts",
8463
+ platform: "MCP"
8464
+ });
8465
+ }
8466
+ const runtimeAuthTargets = [];
8467
+ if (server.auth?.type === "platform") {
8468
+ for (const target of config.targets) {
8469
+ if (target === "claude-code" || target === "cursor" || target === "codex" || target === "opencode") {
8470
+ runtimeAuthTargets.push(target);
8471
+ }
8472
+ }
8473
+ } else {
8474
+ if (claudeUsesPlatformAuth) runtimeAuthTargets.push("claude-code");
8475
+ if (cursorUsesPlatformAuth) runtimeAuthTargets.push("cursor");
8476
+ }
8477
+ if (runtimeAuthTargets.length > 0) {
7950
8478
  pushIssue(issues, {
7951
- level: "error",
7952
- code: "mcp-url-invalid",
7953
- message: `MCP server "${serverName}" has an invalid URL.`,
8479
+ level: "warning",
8480
+ code: "mcp-runtime-auth-external",
8481
+ message: `MCP server "${serverName}" depends on host-managed auth or runtime config on ${runtimeAuthTargets.join(", ")}. Verify the installed bundle in the real host instead of assuming the bundle alone materializes auth.`,
7954
8482
  file: "pluxx.config.ts",
7955
8483
  platform: "MCP"
7956
8484
  });
@@ -7979,10 +8507,11 @@ function lintManifestPromptLimits(config, issues) {
7979
8507
  const prompts = config.brand?.defaultPrompts;
7980
8508
  if (!prompts) continue;
7981
8509
  if (limits.manifestPromptCountMax !== null && prompts.length > limits.manifestPromptCountMax) {
8510
+ const policy = PLATFORM_LIMIT_POLICIES[target].manifestPromptCountMax;
7982
8511
  pushIssue(issues, {
7983
- level: "error",
7984
- code: "platform-prompt-count",
7985
- message: `${target} supports at most ${limits.manifestPromptCountMax} default prompts (found ${prompts.length}).`,
8512
+ level: policy?.kind === "hard" ? "error" : "warning",
8513
+ code: policy?.kind === "hard" ? "platform-prompt-count" : "platform-prompt-count-guideline",
8514
+ message: policy?.kind === "hard" ? `${target} supports at most ${limits.manifestPromptCountMax} default prompts (found ${prompts.length}).` : `Pluxx recommends keeping ${target} default prompts to ${limits.manifestPromptCountMax} or fewer (found ${prompts.length}).`,
7986
8515
  file: "pluxx.config.ts",
7987
8516
  platform: target
7988
8517
  });
@@ -7990,10 +8519,11 @@ function lintManifestPromptLimits(config, issues) {
7990
8519
  if (limits.manifestPromptMax !== null) {
7991
8520
  for (const prompt of prompts) {
7992
8521
  if (prompt.length > limits.manifestPromptMax) {
8522
+ const policy = PLATFORM_LIMIT_POLICIES[target].manifestPromptMax;
7993
8523
  pushIssue(issues, {
7994
- level: "error",
7995
- code: "platform-prompt-length",
7996
- message: `A default prompt exceeds ${target} max of ${limits.manifestPromptMax} characters.`,
8524
+ level: policy?.kind === "hard" ? "error" : "warning",
8525
+ code: policy?.kind === "hard" ? "platform-prompt-length" : "platform-prompt-length-guideline",
8526
+ message: policy?.kind === "hard" ? `A default prompt exceeds ${target} max of ${limits.manifestPromptMax} characters.` : `A default prompt exceeds the Pluxx ${target} compatibility guideline of ${limits.manifestPromptMax} characters.`,
7997
8527
  file: "pluxx.config.ts",
7998
8528
  platform: target
7999
8529
  });
@@ -8183,6 +8713,20 @@ function lintAgentIsolation(agentFiles, issues, frontmatterCache) {
8183
8713
  }
8184
8714
  }
8185
8715
  }
8716
+ function lintOpenCodeAgentFrontmatter(dir, config, issues) {
8717
+ if (!config.targets.includes("opencode") || !config.agents) return;
8718
+ const agents = readCanonicalAgentFiles(resolve9(dir, config.agents));
8719
+ for (const agent of agents) {
8720
+ if (!("tools" in agent.frontmatter)) continue;
8721
+ pushIssue(issues, {
8722
+ level: "warning",
8723
+ code: "opencode-agent-tools-deprecated",
8724
+ message: "OpenCode agent `tools` is deprecated. Pluxx will translate legacy agent tools into permission-first OpenCode output where possible, but canonical agents should prefer `permission`.",
8725
+ file: relative6(dir, agent.filePath).replace(/\\/g, "/"),
8726
+ platform: "OpenCode"
8727
+ });
8728
+ }
8729
+ }
8186
8730
  function lintAbsolutePaths(config, issues) {
8187
8731
  const absolutePathPattern = /^\/[a-zA-Z]|^[A-Z]:\\/;
8188
8732
  if (config.hooks) {
@@ -8324,24 +8868,130 @@ function lintCursorHooks(config, issues) {
8324
8868
  }
8325
8869
  }
8326
8870
  function lintCursorSkillFrontmatter(config, skillFiles, issues, frontmatterCache) {
8327
- if (!config.targets.includes("cursor")) return;
8328
- const cursorSupportedFrontmatter = ["name", "description", "license", "compatibility", "metadata", "disable-model-invocation"];
8871
+ const supportedByTarget = new Map(
8872
+ ["cursor", "codex", "opencode"].filter((target) => config.targets.includes(target)).map((target) => {
8873
+ const rules = getPlatformRules(target);
8874
+ return [target, /* @__PURE__ */ new Set([...rules.frontmatter.standard, ...rules.frontmatter.additional])];
8875
+ })
8876
+ );
8877
+ if (supportedByTarget.size === 0) return;
8329
8878
  for (const skillFile of skillFiles) {
8330
8879
  const { parsed } = getParsedFrontmatterFile(skillFile, frontmatterCache);
8331
8880
  if (!parsed.valid) continue;
8332
8881
  for (const [key] of parsed.fields) {
8333
- if (!cursorSupportedFrontmatter.includes(key)) {
8882
+ for (const [target, supported] of supportedByTarget.entries()) {
8883
+ if (supported.has(key)) continue;
8884
+ const issue = target === "cursor" ? {
8885
+ code: "cursor-skill-frontmatter-unsupported",
8886
+ message: `Skill frontmatter field "${key}" is not supported by Cursor. Supported: ${[...supported].join(", ")}`,
8887
+ platform: "Cursor"
8888
+ } : target === "codex" ? {
8889
+ code: "codex-skill-frontmatter-translation",
8890
+ message: `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.`,
8891
+ platform: "Codex"
8892
+ } : {
8893
+ code: "opencode-skill-frontmatter-translation",
8894
+ message: `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.`,
8895
+ platform: "OpenCode"
8896
+ };
8334
8897
  pushIssue(issues, {
8335
8898
  level: "warning",
8336
- code: "cursor-skill-frontmatter-unsupported",
8337
- message: `Skill frontmatter field "${key}" is not supported by Cursor. Supported: ${cursorSupportedFrontmatter.join(", ")}`,
8338
8899
  file: skillFile,
8339
- platform: "Cursor"
8900
+ ...issue
8340
8901
  });
8341
8902
  }
8342
8903
  }
8343
8904
  }
8344
8905
  }
8906
+ function lintHookFieldTranslations(config, issues) {
8907
+ if (!config.hooks) return;
8908
+ const hasPromptHooks = Object.values(config.hooks).some(
8909
+ (entries) => (entries ?? []).some((entry) => entry.type === "prompt")
8910
+ );
8911
+ const hasFailClosed = Object.values(config.hooks).some(
8912
+ (entries) => (entries ?? []).some((entry) => entry.failClosed !== void 0)
8913
+ );
8914
+ const hasLoopLimit = Object.values(config.hooks).some(
8915
+ (entries) => (entries ?? []).some((entry) => entry.loop_limit !== void 0)
8916
+ );
8917
+ if (hasPromptHooks) {
8918
+ if (config.targets.includes("claude-code")) {
8919
+ pushIssue(issues, {
8920
+ level: "warning",
8921
+ code: "claude-prompt-hook-degrade",
8922
+ 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.",
8923
+ file: "pluxx.config.ts",
8924
+ platform: "claude-code"
8925
+ });
8926
+ }
8927
+ if (config.targets.includes("codex")) {
8928
+ pushIssue(issues, {
8929
+ level: "warning",
8930
+ code: "codex-prompt-hook-drop",
8931
+ message: "Codex currently receives only command-hook companions from Pluxx. Prompt hooks will be dropped from the generated Codex bundle.",
8932
+ file: "pluxx.config.ts",
8933
+ platform: "codex"
8934
+ });
8935
+ }
8936
+ if (config.targets.includes("opencode")) {
8937
+ pushIssue(issues, {
8938
+ level: "warning",
8939
+ code: "opencode-prompt-hook-drop",
8940
+ message: "The current OpenCode runtime wrapper only emits command hooks. Prompt hooks will be dropped from the generated OpenCode plugin.",
8941
+ file: "pluxx.config.ts",
8942
+ platform: "opencode"
8943
+ });
8944
+ }
8945
+ }
8946
+ if (hasFailClosed && config.targets.includes("claude-code")) {
8947
+ pushIssue(issues, {
8948
+ level: "warning",
8949
+ code: "claude-hook-failclosed-degrade",
8950
+ message: "Claude hook entries currently drop `failClosed` in generated output. Keep this behavior host-specific or verify the generated hook bundle carefully.",
8951
+ file: "pluxx.config.ts",
8952
+ platform: "claude-code"
8953
+ });
8954
+ }
8955
+ if (hasLoopLimit) {
8956
+ if (config.targets.includes("claude-code")) {
8957
+ pushIssue(issues, {
8958
+ level: "warning",
8959
+ code: "claude-hook-loop-limit-degrade",
8960
+ message: "Claude outputs currently drop `loop_limit`. Recursive hook protection is not preserved there today.",
8961
+ file: "pluxx.config.ts",
8962
+ platform: "claude-code"
8963
+ });
8964
+ }
8965
+ if (config.targets.includes("codex")) {
8966
+ pushIssue(issues, {
8967
+ level: "warning",
8968
+ code: "codex-hook-loop-limit-drop",
8969
+ message: "Codex hook companions currently drop `loop_limit`. Only command, matcher, timeout, and failClosed survive there today.",
8970
+ file: "pluxx.config.ts",
8971
+ platform: "codex"
8972
+ });
8973
+ }
8974
+ if (config.targets.includes("opencode")) {
8975
+ pushIssue(issues, {
8976
+ level: "warning",
8977
+ code: "opencode-hook-loop-limit-drop",
8978
+ message: "OpenCode runtime hooks currently drop `loop_limit`. Recursive hook protection is still Cursor-first in Pluxx.",
8979
+ file: "pluxx.config.ts",
8980
+ platform: "opencode"
8981
+ });
8982
+ }
8983
+ }
8984
+ }
8985
+ function lintCodexCommandGuidance(config, issues) {
8986
+ if (!config.targets.includes("codex") || !config.commands) return;
8987
+ pushIssue(issues, {
8988
+ level: "warning",
8989
+ code: "codex-commands-routing-guidance",
8990
+ 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.",
8991
+ file: "pluxx.config.ts",
8992
+ platform: "codex"
8993
+ });
8994
+ }
8345
8995
  function lintSkillListingBudgets(skillFiles, targets, issues, frontmatterCache) {
8346
8996
  for (const target of targets) {
8347
8997
  const budget = PLATFORM_LIMITS[target].skillListingBudgetMax;
@@ -8428,12 +9078,12 @@ function lintPermissions(config, issues) {
8428
9078
  });
8429
9079
  }
8430
9080
  if (rules.some((rule) => rule.kind === "Skill")) {
8431
- const nonClaudeTargets = config.targets.filter((target) => target !== "claude-code");
8432
- if (nonClaudeTargets.length > 0) {
9081
+ const limitedTargets = config.targets.filter((target) => !["claude-code", "codex", "opencode"].includes(target));
9082
+ if (limitedTargets.length > 0) {
8433
9083
  pushIssue(issues, {
8434
9084
  level: "warning",
8435
9085
  code: "permissions-skill-selector-limited",
8436
- message: `Skill(...) permission rules are Claude-style and will require downgrade or docs-only handling on ${nonClaudeTargets.join(", ")}.`,
9086
+ message: `Skill(...) permission rules do not have the same native support on ${limitedTargets.join(", ")} and will require downgrade or translation there.`,
8437
9087
  file: "pluxx.config.ts",
8438
9088
  platform: "Permissions"
8439
9089
  });
@@ -8443,7 +9093,7 @@ function lintPermissions(config, issues) {
8443
9093
  pushIssue(issues, {
8444
9094
  level: "warning",
8445
9095
  code: "permissions-opencode-downgrade",
8446
- message: "OpenCode permission output is currently tool-level. Selector patterns like file globs and specific MCP tool names are downgraded to coarse tool permissions there.",
9096
+ message: "OpenCode now preserves most canonical permission selectors natively, but MCP(...) rules still translate through OpenCode tool-name patterns and should be verified against the configured MCP server names.",
8447
9097
  file: "pluxx.config.ts",
8448
9098
  platform: "OpenCode"
8449
9099
  });
@@ -8513,11 +9163,13 @@ async function lintProject(dir = process.cwd(), options = {}) {
8513
9163
  lintSettingsJson(dir, issues);
8514
9164
  lintLegacyCommandsDir(dir, lintConfig, issues);
8515
9165
  lintHookEvents(lintConfig, issues);
8516
- const agentsDir = resolve9(dir, "agents");
9166
+ const agentsDir = resolve9(dir, lintConfig.agents ?? "agents");
8517
9167
  const agentFiles = existsSync17(agentsDir) ? collectMarkdownFiles(agentsDir) : [];
8518
9168
  lintAgentFrontmatter(agentFiles, issues, frontmatterCache);
8519
9169
  lintAgentIsolation(agentFiles, issues, frontmatterCache);
9170
+ lintOpenCodeAgentFrontmatter(dir, { ...lintConfig, agents: lintConfig.agents ?? "./agents/" }, issues);
8520
9171
  lintMcpUrls(lintConfig, issues);
9172
+ lintMcpRuntimeState(lintConfig, issues);
8521
9173
  lintBrandMetadata(lintConfig, issues);
8522
9174
  lintCodexOverrides(lintConfig, issues);
8523
9175
  lintCodexHookCompatibility(lintConfig, issues);
@@ -8525,6 +9177,8 @@ async function lintProject(dir = process.cwd(), options = {}) {
8525
9177
  lintCodexHooksExternalConfig(lintConfig, issues);
8526
9178
  lintPermissions(lintConfig, issues);
8527
9179
  lintPrimitiveTranslations(lintConfig, issues);
9180
+ lintHookFieldTranslations(lintConfig, issues);
9181
+ lintCodexCommandGuidance(lintConfig, issues);
8528
9182
  lintCursorHooks(lintConfig, issues);
8529
9183
  lintCursorRuleContentLimits(lintConfig, issues);
8530
9184
  const skillsDir = resolve9(dir, lintConfig.skills);
@@ -8608,7 +9262,7 @@ import { resolve as resolve11 } from "path";
8608
9262
 
8609
9263
  // src/cli/init-from-mcp.ts
8610
9264
  import { mkdir as mkdir2 } from "fs/promises";
8611
- import { basename as basename4, resolve as resolve10 } from "path";
9265
+ import { basename as basename5, resolve as resolve10 } from "path";
8612
9266
 
8613
9267
  // src/user-config.ts
8614
9268
  var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -9188,7 +9842,7 @@ function buildCommandContent(skill, existingContent) {
9188
9842
  const generatedContent = [
9189
9843
  "---",
9190
9844
  `description: ${JSON.stringify(description)}`,
9191
- `argument-hint: ${JSON.stringify(argumentHint)}`,
9845
+ `argument-hint: ${formatArgumentHintFrontmatter(argumentHint)}`,
9192
9846
  "---",
9193
9847
  "",
9194
9848
  entryBlurb,
@@ -9225,6 +9879,14 @@ function buildCommandContent(skill, existingContent) {
9225
9879
  }
9226
9880
  );
9227
9881
  }
9882
+ function formatArgumentHintFrontmatter(value) {
9883
+ const trimmed = value.trim();
9884
+ if (!trimmed) return '""';
9885
+ if (trimmed.includes("\n") || /(^#)|(\s#)/.test(trimmed)) {
9886
+ return JSON.stringify(trimmed);
9887
+ }
9888
+ return trimmed;
9889
+ }
9228
9890
  function buildInstructionsContent(input) {
9229
9891
  const accessLine = describePluginAccess(input.displayName, input.source, input.runtimeAuthMode ?? "inline");
9230
9892
  const lines = [
@@ -10334,7 +10996,7 @@ function derivePluginName(introspection, source) {
10334
10996
  const candidates = [
10335
10997
  introspection.serverInfo.name,
10336
10998
  introspection.serverInfo.title,
10337
- source.transport === "stdio" ? basename4(source.command) : new URL(source.url).hostname.split(".")[0]
10999
+ source.transport === "stdio" ? basename5(source.command) : new URL(source.url).hostname.split(".")[0]
10338
11000
  ].filter((value) => Boolean(value));
10339
11001
  for (const candidate of candidates) {
10340
11002
  const normalized = toKebabCase(candidate);
@@ -10855,7 +11517,7 @@ function printTestResult(result) {
10855
11517
  }
10856
11518
 
10857
11519
  // src/cli/sync-from-mcp.ts
10858
- import { cpSync as cpSync2, existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, readdirSync as readdirSync6, rmdirSync, writeFileSync as writeFileSync2 } from "fs";
11520
+ import { cpSync as cpSync2, existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, readdirSync as readdirSync6, rmdirSync, writeFileSync as writeFileSync3 } from "fs";
10859
11521
  import { dirname as dirname4, isAbsolute, relative as relative7, resolve as resolve13 } from "path";
10860
11522
  import { tmpdir } from "os";
10861
11523
 
@@ -11138,10 +11800,10 @@ async function createSseClient(server) {
11138
11800
  let resolveEndpoint;
11139
11801
  let rejectEndpoint;
11140
11802
  let endpointSettled = false;
11141
- const endpointReady = new Promise((resolve23, reject) => {
11803
+ const endpointReady = new Promise((resolve24, reject) => {
11142
11804
  resolveEndpoint = (value) => {
11143
11805
  endpointSettled = true;
11144
- resolve23(value);
11806
+ resolve24(value);
11145
11807
  };
11146
11808
  rejectEndpoint = (error) => {
11147
11809
  endpointSettled = true;
@@ -11278,7 +11940,7 @@ async function createSseClient(server) {
11278
11940
  async request(method, params) {
11279
11941
  const requestId = nextRequestId();
11280
11942
  const endpoint = endpointUrl ?? await endpointReady;
11281
- const resultPromise = new Promise((resolve23, reject) => {
11943
+ const resultPromise = new Promise((resolve24, reject) => {
11282
11944
  const timeout = setTimeout(() => {
11283
11945
  pending.delete(requestId);
11284
11946
  reject(new McpIntrospectionError(`Timed out waiting for MCP SSE response to ${method}.`));
@@ -11286,7 +11948,7 @@ async function createSseClient(server) {
11286
11948
  pending.set(requestId, {
11287
11949
  resolve: (value) => {
11288
11950
  clearTimeout(timeout);
11289
- resolve23(value);
11951
+ resolve24(value);
11290
11952
  },
11291
11953
  reject: (error) => {
11292
11954
  clearTimeout(timeout);
@@ -11439,7 +12101,7 @@ async function createStdioClient(server) {
11439
12101
  method,
11440
12102
  ...params ? { params } : {}
11441
12103
  });
11442
- return new Promise((resolve23, reject) => {
12104
+ return new Promise((resolve24, reject) => {
11443
12105
  const timeout = setTimeout(() => {
11444
12106
  pending.delete(id);
11445
12107
  reject(new McpIntrospectionError(`Timed out waiting for MCP stdio response to ${method}.`));
@@ -11447,7 +12109,7 @@ async function createStdioClient(server) {
11447
12109
  pending.set(id, {
11448
12110
  resolve: (value) => {
11449
12111
  clearTimeout(timeout);
11450
- resolve23(value);
12112
+ resolve24(value);
11451
12113
  },
11452
12114
  reject: (error) => {
11453
12115
  clearTimeout(timeout);
@@ -11670,7 +12332,7 @@ async function syncFromMcp(options) {
11670
12332
  if (!existsSync20(newSkillPath)) continue;
11671
12333
  const currentContent = readFileSync8(newSkillPath, "utf-8");
11672
12334
  const updatedContent = injectCustomContent(currentContent, extracted.customContent);
11673
- writeFileSync2(newSkillPath, updatedContent, "utf-8");
12335
+ writeFileSync3(newSkillPath, updatedContent, "utf-8");
11674
12336
  }
11675
12337
  const renamedFiles = [];
11676
12338
  const renamedOldDirs = /* @__PURE__ */ new Set();
@@ -11754,7 +12416,7 @@ async function applyPersistedTaxonomy(rootDir) {
11754
12416
  const instructionsPath = "./INSTRUCTIONS.md";
11755
12417
  const previousInstructions = beforeContents.get(instructionsPath);
11756
12418
  if (previousInstructions !== void 0) {
11757
- writeFileSync2(resolveWithinRoot(rootDir, instructionsPath), previousInstructions, "utf-8");
12419
+ writeFileSync3(resolveWithinRoot(rootDir, instructionsPath), previousInstructions, "utf-8");
11758
12420
  }
11759
12421
  const newMetadataPath = resolveWithinRoot(rootDir, MCP_SCAFFOLD_METADATA_PATH);
11760
12422
  const newMetadata = JSON.parse(readFileSync8(newMetadataPath, "utf-8"));
@@ -11779,7 +12441,7 @@ async function applyPersistedTaxonomy(rootDir) {
11779
12441
  }
11780
12442
  for (const file of afterManaged) {
11781
12443
  if (file === instructionsPath && previousInstructions !== void 0) {
11782
- writeFileSync2(resolveWithinRoot(rootDir, file), previousInstructions, "utf-8");
12444
+ writeFileSync3(resolveWithinRoot(rootDir, file), previousInstructions, "utf-8");
11783
12445
  }
11784
12446
  }
11785
12447
  invalidateSavedAgentPack(rootDir);
@@ -11820,7 +12482,7 @@ function preserveCustomContentForRenames(rootDir, renames, pathForName) {
11820
12482
  if (!existsSync20(newPath)) continue;
11821
12483
  const currentContent = readFileSync8(newPath, "utf-8");
11822
12484
  const updatedContent = injectCustomContent(currentContent, extracted.customContent);
11823
- writeFileSync2(newPath, updatedContent, "utf-8");
12485
+ writeFileSync3(newPath, updatedContent, "utf-8");
11824
12486
  }
11825
12487
  }
11826
12488
  function snapshotManagedFiles(rootDir, files) {
@@ -13073,6 +13735,9 @@ function scoreFirecrawlMappedLink(link, kind) {
13073
13735
  const url = link.url.toLowerCase();
13074
13736
  const text = [link.title, link.description, link.url].filter(Boolean).join(" ").toLowerCase();
13075
13737
  let score = 0;
13738
+ if (url.endsWith(".xml") || text.includes("sitemap")) {
13739
+ return -100;
13740
+ }
13076
13741
  if (kind === "docs") {
13077
13742
  if (url.includes("/mcp")) score += 50;
13078
13743
  if (url.includes("quickstart") || url.includes("get-started")) score += 35;
@@ -13221,9 +13886,9 @@ function buildDocsContextArtifact(sources) {
13221
13886
  const remoteSources = sources.filter((source) => source.status === "ok" && (source.kind === "website" || source.kind === "docs"));
13222
13887
  if (remoteSources.length === 0) return void 0;
13223
13888
  const productName = inferProductName(remoteSources);
13224
- const shortDescription = remoteSources.map((source) => source.description ?? source.paragraphs?.[0]).find((value) => Boolean(value && value.trim()));
13225
- const setupHints = collectHintSentences(remoteSources, ["setup", "install", "get started", "quickstart", "configuration", "configuring", "running", "restart", "onlymaincontent", "main content"]);
13226
- const authHints = collectHintSentences(remoteSources, ["auth", "authentication", "api key", "bearer", "header", "token", "credential"]);
13889
+ const shortDescription = dedupeRepeatedSentences(remoteSources.map((source) => source.description ?? source.paragraphs?.[0]).find((value) => Boolean(value && value.trim())));
13890
+ const setupHints = collectHintSentences(remoteSources, ["setup", "install", "get started", "quickstart", "configuration", "configuring", "running", "restart", "onlymaincontent", "main content", "npx", "npm", "curl", "remote hosted url"]);
13891
+ const authHints = collectHintSentences(remoteSources, ["auth", "authentication", "api key", "api_key", "firecrawl_api_key", "bearer", "header", "token", "credential"]);
13227
13892
  const warnings = collectHintSentences(remoteSources, ["warning", "note", "requires", "must", "if you", "unavailable", "couldn"]);
13228
13893
  const workflowHints = collectWorkflowHints(remoteSources);
13229
13894
  const importantTerms = collectImportantTerms(remoteSources);
@@ -13297,8 +13962,19 @@ function truncateForNote(value, maxLength) {
13297
13962
  }
13298
13963
  function summarizeMarkdownArtifact(content, metadata = {}) {
13299
13964
  const cleaned = content.replace(/\r\n/g, "\n");
13300
- const headings = cleaned.split("\n").map((line) => line.match(/^#{1,3}\s+(.+)$/)?.[1]?.trim()).filter((value) => Boolean(value)).map(stripMarkdownFormatting).filter(Boolean).slice(0, 5);
13301
- const paragraphs = cleaned.split(/\n{2,}/).map((chunk) => stripMarkdownFormatting(chunk)).map((chunk) => chunk.replace(/\s+/g, " ").trim()).filter((chunk) => Boolean(chunk) && !chunk.startsWith("#")).slice(0, 3);
13965
+ const headings = cleaned.split("\n").map((line) => line.match(/^#{1,4}\s+(.+)$/)?.[1]?.trim()).filter((value) => Boolean(value)).map(stripMarkdownFormatting).filter((value) => Boolean(value) && !isLikelyChromeHeading(value)).slice(0, 12);
13966
+ const textChunks = cleaned.split(/\n{2,}/).map((chunk) => stripMarkdownFormatting(chunk)).map((chunk) => chunk.replace(/\s+/g, " ").trim()).filter((chunk) => Boolean(chunk) && !chunk.startsWith("#"));
13967
+ const filteredTextChunks = filterLikelyContentText(textChunks);
13968
+ const codeHints = extractMarkdownCodeHints(cleaned);
13969
+ const paragraphCandidates = uniqueStrings([
13970
+ ...filteredTextChunks,
13971
+ ...codeHints
13972
+ ]);
13973
+ const paragraphs = paragraphCandidates.map((chunk, index) => ({
13974
+ chunk,
13975
+ score: scoreMarkdownTextChunk(chunk),
13976
+ index
13977
+ })).sort((left, right) => right.score - left.score || left.index - right.index).map(({ chunk }) => chunk).slice(0, 5);
13302
13978
  const title = metadata.title?.trim() || headings[0] || paragraphs[0];
13303
13979
  const description = metadata.description?.trim() || paragraphs[0];
13304
13980
  const lines = [];
@@ -13325,6 +14001,42 @@ function summarizeMarkdownArtifact(content, metadata = {}) {
13325
14001
  function stripMarkdownFormatting(value) {
13326
14002
  return value.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]+)`/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^>\s*/gm, "").replace(/^\s*[-*+]\s+/gm, "").replace(/^\s*\d+\.\s+/gm, "").replace(/[_*~#]+/g, " ").replace(/\|/g, " ").replace(/\s+/g, " ").trim();
13327
14003
  }
14004
+ function extractMarkdownCodeHints(content) {
14005
+ const hints = [];
14006
+ for (const match of content.matchAll(/```[^\n]*\n([\s\S]*?)```/g)) {
14007
+ const block = match[1] ?? "";
14008
+ for (const rawLine of block.split("\n")) {
14009
+ const line = rawLine.replace(/\r/g, "").trim();
14010
+ if (!line) continue;
14011
+ if (!/(api[_-]?key|token|header|auth|install|npx|npm|curl|onlymaincontent|main content|map|scrape|crawl|extract|search|agent|command|url|mcp)/i.test(line)) {
14012
+ continue;
14013
+ }
14014
+ const normalized = line.replace(/^[`"'[{(]+|[`"'[\]}):,;]+$/g, "").replace(/\s+/g, " ").trim();
14015
+ if (normalized) {
14016
+ hints.push(normalized);
14017
+ }
14018
+ }
14019
+ }
14020
+ return uniqueStrings(hints).slice(0, 10);
14021
+ }
14022
+ function scoreMarkdownTextChunk(value) {
14023
+ const normalized = value.replace(/\s+/g, " ").trim();
14024
+ if (!normalized) return Number.NEGATIVE_INFINITY;
14025
+ if (isLikelyChromeText(normalized)) return -200;
14026
+ const lower = normalized.toLowerCase();
14027
+ let score = 0;
14028
+ if (/[.!?:]/.test(normalized)) score += 20;
14029
+ if (/`|https?:\/\/|(^| )npx( |$)|(^| )npm( |$)|(^| )curl( |$)|=/.test(normalized)) score += 20;
14030
+ if (lower.includes("search") || lower.includes("scrape") || lower.includes("map") || lower.includes("crawl") || lower.includes("extract") || lower.includes("agent") || lower.includes("browser") || lower.includes("workflow") || lower.includes("knowledge")) {
14031
+ score += 25;
14032
+ }
14033
+ if (lower.includes("api key") || lower.includes("firecrawl_api_key") || lower.includes("token") || lower.includes("auth") || lower.includes("header") || lower.includes("install") || lower.includes("quickstart") || lower.includes("configuration") || lower.includes("onlymaincontent") || lower.includes("main content") || lower.includes("remote hosted url")) {
14034
+ score += 25;
14035
+ }
14036
+ if (normalized.length >= 24 && normalized.length <= 240) score += 10;
14037
+ if (normalized.split(/\s+/).length > 40) score -= 10;
14038
+ return score;
14039
+ }
13328
14040
  function summarizeHtml(html) {
13329
14041
  const cleanedHtml = stripNonContentHtml(html);
13330
14042
  const primaryHtml = selectPrimaryHtmlFragment(cleanedHtml);
@@ -13394,14 +14106,38 @@ function filterLikelyContentText(values) {
13394
14106
  }
13395
14107
  return uniqueStrings(values).map((value) => value.replace(/\s+/g, " ").trim()).filter((value) => value.length > 0);
13396
14108
  }
14109
+ function dedupeRepeatedSentences(value) {
14110
+ if (!value) return void 0;
14111
+ const normalized = value.replace(/\s+/g, " ").replace(/([.!?])\s*,\s*/g, "$1 ").trim();
14112
+ if (!normalized) return void 0;
14113
+ const sentenceMatches = normalized.match(/[^.!?]+[.!?]?/g);
14114
+ if (!sentenceMatches) return normalized;
14115
+ const seen = /* @__PURE__ */ new Set();
14116
+ const uniqueSentences = [];
14117
+ for (const rawSentence of sentenceMatches) {
14118
+ const sentence = rawSentence.trim().replace(/^,+\s*/, "");
14119
+ if (!sentence) continue;
14120
+ const key = sentence.replace(/[.!?]+$/, "").replace(/\s+/g, " ").trim().toLowerCase();
14121
+ if (!key || seen.has(key)) continue;
14122
+ seen.add(key);
14123
+ uniqueSentences.push(sentence);
14124
+ }
14125
+ if (uniqueSentences.length === 0) {
14126
+ return normalized;
14127
+ }
14128
+ return uniqueSentences.join(" ");
14129
+ }
13397
14130
  function isLikelyChromeHeading(value) {
13398
14131
  const normalized = value.replace(/\s+/g, " ").trim();
13399
14132
  if (!normalized) return true;
13400
14133
  const lower = normalized.toLowerCase();
13401
14134
  const chromePatterns = [
13402
14135
  "search docs",
14136
+ "skip to main content",
13403
14137
  "table of contents",
13404
14138
  "on this page",
14139
+ "navigation",
14140
+ "ctrl k",
13405
14141
  "previous",
13406
14142
  "next",
13407
14143
  "privacy",
@@ -13411,6 +14147,8 @@ function isLikelyChromeHeading(value) {
13411
14147
  "blog",
13412
14148
  "pricing",
13413
14149
  "careers",
14150
+ "playground",
14151
+ "community",
13414
14152
  "discord",
13415
14153
  "github",
13416
14154
  "twitter",
@@ -13426,8 +14164,11 @@ function isLikelyChromeText(value) {
13426
14164
  const wordCount = normalized.split(/\s+/).length;
13427
14165
  const chromePatterns = [
13428
14166
  "search docs",
14167
+ "skip to main content",
13429
14168
  "table of contents",
13430
14169
  "on this page",
14170
+ "navigation",
14171
+ "ctrl k",
13431
14172
  "previous",
13432
14173
  "next",
13433
14174
  "privacy",
@@ -13437,6 +14178,8 @@ function isLikelyChromeText(value) {
13437
14178
  "blog",
13438
14179
  "pricing",
13439
14180
  "careers",
14181
+ "playground",
14182
+ "community",
13440
14183
  "discord",
13441
14184
  "github",
13442
14185
  "twitter",
@@ -13563,11 +14306,11 @@ function normalizeProductNameCandidate(value) {
13563
14306
  function collectHintSentences(sources, keywords) {
13564
14307
  const sentences = uniqueStrings(
13565
14308
  sources.flatMap((source) => {
13566
- const text = [
13567
- source.description,
13568
- ...source.paragraphs ?? []
13569
- ].filter(Boolean).join(" ");
13570
- return splitIntoSentences(text).filter((sentence) => keywords.some((keyword) => sentence.toLowerCase().includes(keyword))).slice(0, 4);
14309
+ const segments = [
14310
+ ...source.description ? splitIntoHintSegments(source.description) : [],
14311
+ ...(source.paragraphs ?? []).flatMap(splitIntoHintSegments)
14312
+ ];
14313
+ return segments.filter((sentence) => keywords.some((keyword) => sentence.toLowerCase().includes(keyword))).slice(0, 6);
13571
14314
  })
13572
14315
  );
13573
14316
  return sentences.slice(0, 6);
@@ -13579,16 +14322,50 @@ function collectWorkflowHints(sources) {
13579
14322
  "manual installation",
13580
14323
  "configuration",
13581
14324
  "environment variables",
14325
+ "remote hosted url",
14326
+ "available tools",
13582
14327
  "features",
13583
14328
  "get started",
13584
14329
  "overview",
13585
14330
  "developer guides",
13586
14331
  "quickstarts",
13587
- "mcp server"
14332
+ "mcp server",
14333
+ "ready to build?",
14334
+ "ready to build",
14335
+ "use well-known tools",
14336
+ "code you can trust",
14337
+ "frequently asked questions"
13588
14338
  ]);
13589
- return uniqueStrings(
14339
+ const workflowKeywords = ["search", "scrape", "map", "crawl", "extract", "agent", "browser", "workflow", "knowledge"];
14340
+ const workflowStopKeywords = ["install", "auth", "token", "header", "configuration", "quickstart"];
14341
+ const sourceTitles = new Set(
14342
+ sources.flatMap((source) => source.title ? [source.title.toLowerCase()] : []).filter(Boolean)
14343
+ );
14344
+ const headingHints = uniqueStrings(
13590
14345
  sources.flatMap((source) => source.headings ?? [])
13591
- ).map((heading) => heading.replace(/\s+/g, " ").trim()).filter((heading) => heading.length > 0 && heading.split(/\s+/).length <= 4 && !genericHeadings.has(heading.toLowerCase())).slice(0, 8);
14346
+ ).map(normalizeHintText).filter(
14347
+ (heading) => heading.length > 0 && heading.split(/\s+/).length <= 4 && !genericHeadings.has(heading.toLowerCase()) && (!sourceTitles.has(heading.toLowerCase()) || workflowKeywords.some((keyword) => containsWholeWordKeyword(heading, keyword))) && !heading.includes("://")
14348
+ ).map((heading, index) => ({
14349
+ value: heading,
14350
+ index,
14351
+ score: scoreWorkflowHint(heading, workflowKeywords)
14352
+ })).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).map((entry) => entry.value);
14353
+ const paragraphHints = uniqueStrings(
14354
+ sources.flatMap(
14355
+ (source) => (source.paragraphs ?? []).flatMap(splitIntoHintSegments).filter((segment) => {
14356
+ const lower = segment.toLowerCase();
14357
+ return workflowKeywords.some((keyword) => containsWholeWordKeyword(lower, keyword)) && !workflowStopKeywords.some((keyword) => lower.includes(keyword)) && !segment.includes("://") && !segment.includes("=");
14358
+ })
14359
+ )
14360
+ ).map(normalizeHintText).map((segment, index) => ({
14361
+ value: segment,
14362
+ index,
14363
+ score: scoreWorkflowHint(segment, workflowKeywords)
14364
+ })).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).map((entry) => entry.value);
14365
+ return uniqueStrings([
14366
+ ...headingHints,
14367
+ ...paragraphHints
14368
+ ]).slice(0, 8);
13592
14369
  }
13593
14370
  function collectImportantTerms(sources) {
13594
14371
  return uniqueStrings(
@@ -13598,8 +14375,34 @@ function collectImportantTerms(sources) {
13598
14375
  ])
13599
14376
  ).map((term) => term.replace(/\s+/g, " ").trim()).filter((term) => term.length > 0 && term.split(/\s+/).length <= 4).slice(0, 8);
13600
14377
  }
13601
- function splitIntoSentences(value) {
13602
- return value.split(/(?<=[.!?])\s+/).map((sentence) => sentence.replace(/\s+/g, " ").trim()).filter(Boolean);
14378
+ function splitIntoHintSegments(value) {
14379
+ return value.split(/(?<=[.!?])\s+|\n+/).map(normalizeHintText).filter(Boolean);
14380
+ }
14381
+ function normalizeHintText(value) {
14382
+ return value.replace(/[\u200B-\u200D\uFEFF]/g, " ").replace(/^(?:hashtag\s+)+/gi, "").replace(/^(?:[#>*-]+\s*)+/, "").replace(/[\\]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[`*_]+/g, " ").replace(/\s+/g, " ").trim();
14383
+ }
14384
+ function scoreWorkflowHint(value, workflowKeywords) {
14385
+ const normalized = normalizeHintText(value);
14386
+ const lower = normalized.toLowerCase();
14387
+ let score = 0;
14388
+ for (const keyword of workflowKeywords) {
14389
+ if (containsWholeWordKeyword(lower, keyword)) {
14390
+ score += 30;
14391
+ } else if (lower.includes(keyword)) {
14392
+ score += 10;
14393
+ }
14394
+ }
14395
+ if (lower.includes("feature") || lower.includes("ready to build") || lower.includes("faq") || lower.includes("question")) {
14396
+ score -= 20;
14397
+ }
14398
+ if (normalized.split(/\s+/).length <= 3) {
14399
+ score += 5;
14400
+ }
14401
+ return score;
14402
+ }
14403
+ function containsWholeWordKeyword(value, keyword) {
14404
+ const pattern = new RegExp(`\\b${keyword.replace(/\s+/g, "\\s+")}\\b`, "i");
14405
+ return pattern.test(value);
13603
14406
  }
13604
14407
  function uniqueStrings(values) {
13605
14408
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
@@ -13656,17 +14459,22 @@ function buildAgentPrompt(kind, input) {
13656
14459
  2. Infer the MCP's real product surfaces and workflows from tools, resources, resource templates, and prompt templates.
13657
14460
  3. Merge, split, or rename generated skills so labels are product-facing, not lexical buckets.
13658
14461
  4. Update the taxonomy file first; Pluxx will re-render generated skills and commands from that taxonomy after the pass.
13659
- 5. Keep setup/onboarding, account-admin, and runtime workflows intentionally separated when appropriate.
13660
- 6. Eliminate misleading labels such as contact or people discovery when the tools do not actually perform direct lookup.
13661
- 7. Use per-skill related resources and prompt templates as strong evidence for workflow shape, but correct them when broader discovery evidence shows a mismatch.
13662
- 8. Reject stale scaffold assumptions; if current files conflict with discovery context, prefer the discovery evidence and flag the mismatch.
14462
+ 5. Promote clear, repeated user entrypoints into explicit commands when host-native command UX would be clearer than asking users to infer the right skill.
14463
+ 6. Promote specialist, delegated, reviewer, or bounded-execution workflows into agents/subagents when isolation is a better native fit than another inline skill.
14464
+ 7. Keep setup/onboarding, account-admin, and runtime workflows intentionally separated when appropriate.
14465
+ 8. Eliminate misleading labels such as contact or people discovery when the tools do not actually perform direct lookup.
14466
+ 9. Use per-skill related resources and prompt templates as strong evidence for workflow shape, but correct them when broader discovery evidence shows a mismatch.
14467
+ 10. When prompt templates or tool schemas imply a parameterized workflow, preserve that intent through realistic arguments, argument-bearing commands, and runnable entrypoints instead of flattening everything into generic skills.
14468
+ 11. Reject stale scaffold assumptions; if current files conflict with discovery context, prefer the discovery evidence and flag the mismatch.
14469
+ 12. Do not settle for a least-common-denominator skill-only scaffold if the product would be more native with commands, agents, or richer argument shapes.
13663
14470
  ${buildPromptOverrideBlock(kind, input.overrides)}
13664
14471
  Success criteria:
13665
14472
  - each skill represents a real user workflow or product surface
13666
14473
  - skill names are product-shaped and avoid raw MCP tool/server identifiers when possible
13667
14474
  - setup/onboarding, account-admin, and runtime workflows are grouped intentionally
13668
14475
  - singleton skills are avoided unless they represent a real standalone user workflow
13669
- - commands stay aligned with the chosen taxonomy and avoid weak command UX
14476
+ - commands stay aligned with the chosen taxonomy, avoid weak command UX, and use realistic arguments when workflows are parameterized
14477
+ - specialist or delegated workflows are promoted into agents/subagents when that native shape is stronger than another flat skill
13670
14478
  - per-skill resource and prompt-template associations remain coherent with the chosen taxonomy
13671
14479
  - taxonomy decisions are grounded in current discovery context, not stale scaffold assumptions
13672
14480
  `;
@@ -13679,7 +14487,9 @@ Success criteria:
13679
14487
  4. Keep wording aligned to the MCP's product narrative and branded language; avoid raw MCP server/tool identifiers except when technically required.
13680
14488
  5. Prefer the branded product name in user-facing copy; do not lead with internal MCP server identifiers.
13681
14489
  6. Replace stale scaffold claims with current discovery-backed language and keep command examples operational, concrete, and copy-paste runnable.
13682
- 7. When a workflow already has related resources or prompt templates in the context, keep the wording and examples aligned to that surfaced workflow evidence.
14490
+ 7. When discovery implies a parameterized workflow, make the examples show realistic arguments instead of bare placeholder commands.
14491
+ 8. Call out when a request should route to a specialist agent/subagent instead of the generic skill path.
14492
+ 9. When a workflow already has related resources or prompt templates in the context, keep the wording and examples aligned to that surfaced workflow evidence.
13683
14493
  ${buildPromptOverrideBlock(kind, input.overrides)}
13684
14494
  Success criteria:
13685
14495
  - instructions are concise, actionable, and product-shaped
@@ -13688,20 +14498,21 @@ Success criteria:
13688
14498
  - raw MCP server identifiers are omitted unless operationally necessary
13689
14499
  - the generated section reads like routing guidance, not pasted vendor docs
13690
14500
  - command examples use strong command UX (clear intent, realistic args, and runnable shapes)
14501
+ - specialist routing is explicit when certain work should go to an agent/subagent instead of a generic skill
13691
14502
  - workflow guidance stays coherent with related resource and prompt-template evidence in the context
13692
14503
  - the file remains safe for future \`pluxx sync --from-mcp\`
13693
14504
  `;
13694
14505
  }
13695
14506
  return `${sharedIntro.join("\n")}Your job:
13696
14507
  1. Review the current scaffold critically.
13697
- 2. Call out weak skill groupings, missing setup guidance, vague examples, product/category mismatches, raw documentation dumps, lexical skill names, stale scaffold assumptions, weak command UX${input.sourceKind === "mcp-derived" ? ", incoherent per-skill resource/prompt associations, or weak MCP metadata signals" : ", weak marketplace/listing copy, awkward installation guidance, or unclear operator boundaries"}.
14508
+ 2. Call out weak skill groupings, missing setup guidance, vague examples, product/category mismatches, raw documentation dumps, lexical skill names, stale scaffold assumptions, weak command UX, missing argument-bearing command entrypoints, missing specialist agent/subagent boundaries, lowest-common-denominator skill-only scaffolds, and misplaced Claude-only skill-frontmatter intent${input.sourceKind === "mcp-derived" ? ", incoherent per-skill resource/prompt associations, or weak MCP metadata signals" : ", weak marketplace/listing copy, awkward installation guidance, or unclear operator boundaries"}.
13698
14509
  3. Separate scaffold quality findings from runtime-correctness findings.
13699
14510
  4. Propose only the highest-value changes needed to make the scaffold useful.
13700
14511
  ${buildPromptOverrideBlock(kind, input.overrides)}
13701
14512
  Success criteria:
13702
14513
  - findings are concrete and tied to files
13703
14514
  - scaffold quality gaps are distinguished from runtime correctness
13704
- - stale assumptions${input.sourceKind === "mcp-derived" ? ", incoherent per-skill discovery associations," : ","} and command-UX weaknesses are identified explicitly when present
14515
+ - stale assumptions${input.sourceKind === "mcp-derived" ? ", incoherent per-skill discovery associations," : ","} command-UX weaknesses, and missing agent/subagent shaping are identified explicitly when present
13705
14516
  - suggested changes improve user-facing plugin quality
13706
14517
  - recommendations stay inside Pluxx-managed boundaries
13707
14518
  `;
@@ -14306,11 +15117,11 @@ ${additions.map((block) => `- ${block.replace(/\n/g, "\n ")}`).join("\n")}
14306
15117
 
14307
15118
  // src/cli/doctor.ts
14308
15119
  import { accessSync, constants, existsSync as existsSync23, lstatSync, readFileSync as readFileSync11, readdirSync as readdirSync9 } from "fs";
14309
- import { basename as basename5, dirname as dirname6, resolve as resolve16 } from "path";
15120
+ import { basename as basename6, dirname as dirname6, resolve as resolve16 } from "path";
14310
15121
 
14311
15122
  // src/cli/install.ts
14312
15123
  import { resolve as resolve15, dirname as dirname5 } from "path";
14313
- import { existsSync as existsSync22, symlinkSync, mkdirSync as mkdirSync3, rmSync as rmSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync3, cpSync as cpSync3, readdirSync as readdirSync8 } from "fs";
15124
+ import { existsSync as existsSync22, symlinkSync, mkdirSync as mkdirSync4, rmSync as rmSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4, cpSync as cpSync3, readdirSync as readdirSync8 } from "fs";
14314
15125
  import { spawnSync } from "child_process";
14315
15126
  import * as readline2 from "readline";
14316
15127
  function listHookCommands(hooks) {
@@ -14476,7 +15287,7 @@ function getInstallTargets(pluginName) {
14476
15287
  {
14477
15288
  platform: "opencode",
14478
15289
  pluginDir: resolve15(home, ".config/opencode/plugins", pluginName),
14479
- description: `~/.config/opencode/plugins/${pluginName}.ts`
15290
+ description: `~/.config/opencode/plugins/${pluginName}.ts + ~/.config/opencode/plugins/${pluginName}/`
14480
15291
  },
14481
15292
  {
14482
15293
  platform: "github-copilot",
@@ -14524,7 +15335,7 @@ function toPascalCase2(value) {
14524
15335
  function writeOpenCodeEntryFile(pluginDir, pluginName) {
14525
15336
  const entryPath = getOpenCodeEntryPath(pluginDir);
14526
15337
  const exportName = toPascalCase2(pluginName);
14527
- writeFileSync3(
15338
+ writeFileSync4(
14528
15339
  entryPath,
14529
15340
  [
14530
15341
  'import type { Plugin } from "@opencode-ai/plugin"',
@@ -14580,7 +15391,7 @@ ${content.slice(frontmatterMatch[0].length)}`;
14580
15391
  function syncOpenCodeSkills(pluginDir, pluginName) {
14581
15392
  const sourceSkillsDir = resolve15(pluginDir, "skills");
14582
15393
  if (!existsSync22(sourceSkillsDir)) return;
14583
- mkdirSync3(getOpenCodeSkillRoot(), { recursive: true });
15394
+ mkdirSync4(getOpenCodeSkillRoot(), { recursive: true });
14584
15395
  for (const entry of readdirSync8(sourceSkillsDir, { withFileTypes: true })) {
14585
15396
  if (!entry.isDirectory()) continue;
14586
15397
  const skillSourceDir = resolve15(sourceSkillsDir, entry.name);
@@ -14589,7 +15400,7 @@ function syncOpenCodeSkills(pluginDir, pluginName) {
14589
15400
  rmSync3(installedSkillDir, { recursive: true, force: true });
14590
15401
  cpSync3(skillSourceDir, installedSkillDir, { recursive: true });
14591
15402
  const skillPath = resolve15(installedSkillDir, "SKILL.md");
14592
- writeFileSync3(
15403
+ writeFileSync4(
14593
15404
  skillPath,
14594
15405
  namespaceOpenCodeSkill(readFileSync10(skillPath, "utf-8"), pluginName, entry.name)
14595
15406
  );
@@ -14641,6 +15452,15 @@ function getInstallFollowupNotes(platforms) {
14641
15452
  if (platforms.includes("claude-code")) {
14642
15453
  notes.push("Claude Code note: if Claude is already open, run /reload-plugins in the session to pick up the new install.");
14643
15454
  }
15455
+ if (platforms.includes("cursor")) {
15456
+ notes.push("Cursor note: if Cursor is already open, use Developer: Reload Window or restart Cursor to pick up the new install.");
15457
+ }
15458
+ if (platforms.includes("codex")) {
15459
+ notes.push("Codex note: if Codex is already open, use Plugins > Refresh if that action is available in your current UI, or restart Codex to pick up the new install.");
15460
+ }
15461
+ if (platforms.includes("opencode")) {
15462
+ notes.push("OpenCode note: if OpenCode is already open, restart or reload it so the plugin is picked up.");
15463
+ }
14644
15464
  return notes;
14645
15465
  }
14646
15466
  function runCommandDefault(command2, args2) {
@@ -14653,7 +15473,7 @@ function runCommandDefault(command2, args2) {
14653
15473
  }
14654
15474
  function createSymlinkInstall(target) {
14655
15475
  const parentDir = resolve15(target.pluginDir, "..");
14656
- mkdirSync3(parentDir, { recursive: true });
15476
+ mkdirSync4(parentDir, { recursive: true });
14657
15477
  if (existsSync22(target.pluginDir)) {
14658
15478
  rmSync3(target.pluginDir, { recursive: true, force: true });
14659
15479
  }
@@ -14691,7 +15511,7 @@ function readCodexMarketplace(filepath) {
14691
15511
  }
14692
15512
  function ensureCodexMarketplace(pluginName) {
14693
15513
  const filepath = getCodexMarketplacePath();
14694
- mkdirSync3(dirname5(filepath), { recursive: true });
15514
+ mkdirSync4(dirname5(filepath), { recursive: true });
14695
15515
  const marketplace = readCodexMarketplace(filepath);
14696
15516
  const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName);
14697
15517
  nextPlugins.push({
@@ -14706,7 +15526,7 @@ function ensureCodexMarketplace(pluginName) {
14706
15526
  },
14707
15527
  category: "Productivity"
14708
15528
  });
14709
- writeFileSync3(
15529
+ writeFileSync4(
14710
15530
  filepath,
14711
15531
  JSON.stringify({
14712
15532
  name: marketplace.name ?? "pluxx-local",
@@ -14727,7 +15547,7 @@ function removeCodexMarketplacePlugin(pluginName) {
14727
15547
  rmSync3(filepath, { force: true });
14728
15548
  return;
14729
15549
  }
14730
- writeFileSync3(
15550
+ writeFileSync4(
14731
15551
  filepath,
14732
15552
  JSON.stringify({
14733
15553
  name: marketplace.name ?? "pluxx-local",
@@ -14738,7 +15558,7 @@ function removeCodexMarketplacePlugin(pluginName) {
14738
15558
  }
14739
15559
  function createCopiedInstall(target) {
14740
15560
  const parentDir = resolve15(target.pluginDir, "..");
14741
- mkdirSync3(parentDir, { recursive: true });
15561
+ mkdirSync4(parentDir, { recursive: true });
14742
15562
  if (existsSync22(target.pluginDir)) {
14743
15563
  rmSync3(target.pluginDir, { recursive: true, force: true });
14744
15564
  }
@@ -14791,7 +15611,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
14791
15611
  }
14792
15612
  mcpServers[name] = entry;
14793
15613
  }
14794
- writeFileSync3(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
15614
+ writeFileSync4(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
14795
15615
  return;
14796
15616
  }
14797
15617
  if (platform === "codex") {
@@ -14821,7 +15641,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
14821
15641
  }
14822
15642
  mcpServers[name] = entry;
14823
15643
  }
14824
- writeFileSync3(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
15644
+ writeFileSync4(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
14825
15645
  }
14826
15646
  }
14827
15647
  function writeInstalledUserConfig(pluginDir, entries) {
@@ -14831,13 +15651,13 @@ function writeInstalledUserConfig(pluginDir, entries) {
14831
15651
  values: buildUserConfigValueMap(entries),
14832
15652
  env: buildUserConfigEnvMap(entries)
14833
15653
  };
14834
- writeFileSync3(filepath, JSON.stringify(payload, null, 2) + "\n");
15654
+ writeFileSync4(filepath, JSON.stringify(payload, null, 2) + "\n");
14835
15655
  }
14836
15656
  function disableInstalledEnvValidation(pluginDir, entries) {
14837
15657
  if (entries.length === 0) return;
14838
15658
  const filepath = resolve15(pluginDir, "scripts/check-env.sh");
14839
15659
  if (!existsSync22(filepath)) return;
14840
- writeFileSync3(
15660
+ writeFileSync4(
14841
15661
  filepath,
14842
15662
  "#!/usr/bin/env bash\nset -euo pipefail\n# pluxx install materialized required config for this local plugin install.\nexit 0\n"
14843
15663
  );
@@ -14869,15 +15689,15 @@ function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
14869
15689
  const pluginManifestPath = resolve15(sourceDir, ".claude-plugin/plugin.json");
14870
15690
  const pluginManifest = JSON.parse(readFileSync10(pluginManifestPath, "utf-8"));
14871
15691
  rmSync3(marketplaceRoot, { recursive: true, force: true });
14872
- mkdirSync3(marketplaceManifestDir, { recursive: true });
14873
- mkdirSync3(resolve15(marketplaceRoot, "plugins"), { recursive: true });
15692
+ mkdirSync4(marketplaceManifestDir, { recursive: true });
15693
+ mkdirSync4(resolve15(marketplaceRoot, "plugins"), { recursive: true });
14874
15694
  if (materialized && materialized.entries.length > 0) {
14875
15695
  cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
14876
15696
  materializeInstalledPlugin(marketplacePluginDir, "claude-code", materialized.config, materialized.entries);
14877
15697
  } else {
14878
15698
  symlinkSync(sourceDir, marketplacePluginDir);
14879
15699
  }
14880
- writeFileSync3(
15700
+ writeFileSync4(
14881
15701
  resolve15(marketplaceManifestDir, "marketplace.json"),
14882
15702
  JSON.stringify({
14883
15703
  name: marketplaceName,
@@ -15473,6 +16293,43 @@ function checkMcpMetadataQuality(checks, metadata) {
15473
16293
  path: MCP_SCAFFOLD_METADATA_PATH
15474
16294
  });
15475
16295
  }
16296
+ function checkCompilerIntent(checks, rootDir) {
16297
+ try {
16298
+ const compilerIntent = readCompilerIntent(rootDir);
16299
+ if (!compilerIntent) return;
16300
+ if ((compilerIntent.skillPolicies?.length ?? 0) === 0) {
16301
+ addCheck2(checks, {
16302
+ level: "info",
16303
+ code: "compiler-intent-empty",
16304
+ title: "Compiler intent file present",
16305
+ detail: `${PLUXX_COMPILER_INTENT_PATH} exists but does not currently carry migrated source-host policy rows.`,
16306
+ fix: "No action needed unless you expected migrated source-host policy to survive into generated outputs.",
16307
+ path: PLUXX_COMPILER_INTENT_PATH
16308
+ });
16309
+ return;
16310
+ }
16311
+ const sourceLabels = [...new Set(
16312
+ compilerIntent.skillPolicies.map((policy) => `${policy.source.platform}:${policy.source.kind}`)
16313
+ )];
16314
+ addCheck2(checks, {
16315
+ level: "info",
16316
+ code: "compiler-intent-source-host",
16317
+ title: "Imported source-host intent still influences compilation",
16318
+ detail: `${PLUXX_COMPILER_INTENT_PATH} preserves ${compilerIntent.skillPolicies.length} migrated skill polic${compilerIntent.skillPolicies.length === 1 ? "y" : "ies"} from ${sourceLabels.join(", ")} so Pluxx can translate that source-host intent into native target surfaces.`,
16319
+ fix: "Review the generated permissions, agents, and host companions to confirm the migrated source-host assumptions still match the plugin you want to ship.",
16320
+ path: PLUXX_COMPILER_INTENT_PATH
16321
+ });
16322
+ } catch (error) {
16323
+ addCheck2(checks, {
16324
+ level: "warning",
16325
+ code: "compiler-intent-invalid",
16326
+ title: "Compiler intent file could not be parsed",
16327
+ detail: error instanceof Error ? error.message : String(error),
16328
+ fix: `Repair or remove ${PLUXX_COMPILER_INTENT_PATH} and rerun pluxx doctor.`,
16329
+ path: PLUXX_COMPILER_INTENT_PATH
16330
+ });
16331
+ }
16332
+ }
15476
16333
  function checkScaffoldMetadata(checks, rootDir, config) {
15477
16334
  const metadataPath = resolve16(rootDir, MCP_SCAFFOLD_METADATA_PATH);
15478
16335
  if (!existsSync23(metadataPath)) {
@@ -15805,7 +16662,7 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
15805
16662
  function isLikelyOpenCodeInstallPath(rootDir) {
15806
16663
  const parent = dirname6(rootDir);
15807
16664
  const grandparent = dirname6(parent);
15808
- return basename5(parent) === "plugins" && basename5(grandparent) === "opencode";
16665
+ return basename6(parent) === "plugins" && basename6(grandparent) === "opencode";
15809
16666
  }
15810
16667
  function checkInstalledOpenCodeHostBridge(checks, rootDir) {
15811
16668
  if (!isLikelyOpenCodeInstallPath(rootDir)) {
@@ -15819,7 +16676,7 @@ function checkInstalledOpenCodeHostBridge(checks, rootDir) {
15819
16676
  });
15820
16677
  return;
15821
16678
  }
15822
- const pluginName = basename5(rootDir);
16679
+ const pluginName = basename6(rootDir);
15823
16680
  const entryPath = `${rootDir}.ts`;
15824
16681
  const entryRelativePath = `${pluginName}.ts`;
15825
16682
  if (!existsSync23(entryPath)) {
@@ -15860,7 +16717,7 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
15860
16717
  if (!isLikelyOpenCodeInstallPath(rootDir)) {
15861
16718
  return;
15862
16719
  }
15863
- const pluginName = basename5(rootDir);
16720
+ const pluginName = basename6(rootDir);
15864
16721
  const sourceSkillsDir = resolve16(rootDir, "skills");
15865
16722
  if (!existsSync23(sourceSkillsDir)) {
15866
16723
  addCheck2(checks, {
@@ -16033,6 +16890,7 @@ async function doctorProject(rootDir = process.cwd()) {
16033
16890
  checkMcpConfig(checks, config);
16034
16891
  checkUserConfig(checks, config);
16035
16892
  checkScaffoldMetadata(checks, rootDir, config);
16893
+ checkCompilerIntent(checks, rootDir);
16036
16894
  checkHookTrust(checks, config);
16037
16895
  for (const target of config.targets) {
16038
16896
  const limits = PLATFORM_LIMITS[target];
@@ -16159,8 +17017,8 @@ async function runBuild(rootDir, targets) {
16159
17017
  }
16160
17018
 
16161
17019
  // src/cli/migrate.ts
16162
- import { basename as basename6, relative as relative10, resolve as resolve18 } from "path";
16163
- import { existsSync as existsSync24, readdirSync as readdirSync10, mkdirSync as mkdirSync4, cpSync as cpSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
17020
+ import { basename as basename7, relative as relative10, resolve as resolve18 } from "path";
17021
+ import { existsSync as existsSync24, readdirSync as readdirSync10, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
16164
17022
  function detectPlatform(pluginDir) {
16165
17023
  const checks = [
16166
17024
  { dir: ".claude-plugin", platform: "claude-code" },
@@ -16622,7 +17480,7 @@ function sanitizeMigratedSkillFrontmatter(outputDir) {
16622
17480
  "---",
16623
17481
  ...lines.slice(endIndex + 1)
16624
17482
  ].join("\n");
16625
- writeFileSync4(skillPath, rewritten, "utf-8");
17483
+ writeFileSync5(skillPath, rewritten, "utf-8");
16626
17484
  }
16627
17485
  }
16628
17486
  function readTomlScalarValue(content, key) {
@@ -16646,16 +17504,6 @@ function renderMigratedAgentMarkdown(fileStem, parsed) {
16646
17504
  const agentName = toKebabCase2(parsed.name ?? fileStem) || "agent";
16647
17505
  const title = parsed.name ?? titleCaseFromDirName(agentName);
16648
17506
  const bodyLines = [];
16649
- if (parsed.model || parsed.effort) {
16650
- bodyLines.push("Source metadata:");
16651
- if (parsed.model) {
16652
- bodyLines.push(`- Preferred model: \`${parsed.model}\``);
16653
- }
16654
- if (parsed.effort) {
16655
- bodyLines.push(`- Preferred reasoning effort: \`${parsed.effort}\``);
16656
- }
16657
- bodyLines.push("");
16658
- }
16659
17507
  if (parsed.developerInstructions) {
16660
17508
  bodyLines.push(parsed.developerInstructions.trim());
16661
17509
  } else {
@@ -16665,6 +17513,8 @@ function renderMigratedAgentMarkdown(fileStem, parsed) {
16665
17513
  "---",
16666
17514
  `name: ${JSON.stringify(agentName)}`,
16667
17515
  ...parsed.description ? [`description: ${JSON.stringify(parsed.description)}`] : [],
17516
+ ...parsed.model ? [`model: ${JSON.stringify(parsed.model)}`] : [],
17517
+ ...parsed.effort ? [`model_reasoning_effort: ${JSON.stringify(parsed.effort)}`] : [],
16668
17518
  "---",
16669
17519
  "",
16670
17520
  `# ${title}`,
@@ -16716,7 +17566,7 @@ function hasTopLevelFrontmatterKey(frontmatterLines, key) {
16716
17566
  function normalizeMigratedOpenCodeAgentFile(agentPath) {
16717
17567
  const original = readFileSync12(agentPath, "utf-8");
16718
17568
  const parsed = splitMarkdownFrontmatter4(original);
16719
- const fileStem = toKebabCase2(basename6(agentPath, ".md")) || "agent";
17569
+ const fileStem = toKebabCase2(basename7(agentPath, ".md")) || "agent";
16720
17570
  const fallbackDescription = buildFallbackAgentDescription(fileStem);
16721
17571
  if (!parsed.hasFrontmatter) {
16722
17572
  const rewritten2 = [
@@ -16728,7 +17578,7 @@ function normalizeMigratedOpenCodeAgentFile(agentPath) {
16728
17578
  original.trimEnd(),
16729
17579
  ""
16730
17580
  ].join("\n");
16731
- writeFileSync4(agentPath, rewritten2, "utf-8");
17581
+ writeFileSync5(agentPath, rewritten2, "utf-8");
16732
17582
  return true;
16733
17583
  }
16734
17584
  const additions = [];
@@ -16749,7 +17599,7 @@ function normalizeMigratedOpenCodeAgentFile(agentPath) {
16749
17599
  "---",
16750
17600
  parsed.body
16751
17601
  ].join("\n");
16752
- writeFileSync4(agentPath, rewritten, "utf-8");
17602
+ writeFileSync5(agentPath, rewritten, "utf-8");
16753
17603
  return true;
16754
17604
  }
16755
17605
  function walkMarkdownFiles3(dir) {
@@ -16781,13 +17631,13 @@ function copyCodexAgents(sourceDir, destDir) {
16781
17631
  const entries = readdirSync10(sourceDir, { withFileTypes: true });
16782
17632
  const tomlEntries = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".toml"));
16783
17633
  if (tomlEntries.length === 0) return false;
16784
- mkdirSync4(destDir, { recursive: true });
17634
+ mkdirSync5(destDir, { recursive: true });
16785
17635
  for (const entry of tomlEntries) {
16786
17636
  const sourcePath = resolve18(sourceDir, entry.name);
16787
17637
  const parsed = parseCodexAgentToml(readFileSync12(sourcePath, "utf-8"));
16788
17638
  const fallbackName = entry.name.replace(/\.toml$/, "");
16789
17639
  const fileName = `${toKebabCase2(parsed.name ?? fallbackName) || "agent"}.md`;
16790
- writeFileSync4(resolve18(destDir, fileName), renderMigratedAgentMarkdown(fallbackName, parsed), "utf-8");
17640
+ writeFileSync5(resolve18(destDir, fileName), renderMigratedAgentMarkdown(fallbackName, parsed), "utf-8");
16791
17641
  }
16792
17642
  return true;
16793
17643
  }
@@ -17179,7 +18029,7 @@ Generated pluxx.config.ts`);
17179
18029
  }
17180
18030
  const taxonomyPath = resolve18(outputDir, MCP_TAXONOMY_PATH);
17181
18031
  const metadataPath = resolve18(outputDir, MCP_SCAFFOLD_METADATA_PATH);
17182
- mkdirSync4(resolve18(outputDir, ".pluxx"), { recursive: true });
18032
+ mkdirSync5(resolve18(outputDir, ".pluxx"), { recursive: true });
17183
18033
  await writeTextFile(taxonomyPath, `${JSON.stringify(result.persistedSkills, null, 2)}
17184
18034
  `);
17185
18035
  if (result.compilerIntent) {
@@ -17206,7 +18056,7 @@ Generated pluxx.config.ts`);
17206
18056
  }
17207
18057
 
17208
18058
  // src/cli/mcp-proxy.ts
17209
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync13 } from "fs";
18059
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync13 } from "fs";
17210
18060
  import { dirname as dirname7, resolve as resolve19 } from "path";
17211
18061
  import * as readline3 from "readline";
17212
18062
  function usage() {
@@ -17284,7 +18134,7 @@ async function loadReplayTape(filepath) {
17284
18134
  }
17285
18135
  async function writeTape(filepath, tape) {
17286
18136
  const absolutePath = resolve19(process.cwd(), filepath);
17287
- mkdirSync5(dirname7(absolutePath), { recursive: true });
18137
+ mkdirSync6(dirname7(absolutePath), { recursive: true });
17288
18138
  await writeTextFile(absolutePath, `${JSON.stringify(tape, null, 2)}
17289
18139
  `);
17290
18140
  }
@@ -17455,7 +18305,7 @@ var PromptCancelledError = class extends Error {
17455
18305
  }
17456
18306
  };
17457
18307
  function ask(question) {
17458
- return new Promise((resolve23, reject) => {
18308
+ return new Promise((resolve24, reject) => {
17459
18309
  const rl = readline4.createInterface({
17460
18310
  input: process.stdin,
17461
18311
  output: process.stdout
@@ -17483,7 +18333,7 @@ function ask(question) {
17483
18333
  rl.once("close", onClose);
17484
18334
  rl.question(question, (answer) => {
17485
18335
  settle(() => {
17486
- resolve23(answer);
18336
+ resolve24(answer);
17487
18337
  rl.close();
17488
18338
  });
17489
18339
  });
@@ -18458,13 +19308,13 @@ ${c2}
18458
19308
  } }).prompt();
18459
19309
 
18460
19310
  // src/cli/index.ts
18461
- import { basename as basename7, resolve as resolve22 } from "path";
18462
- import { mkdir as mkdir4, mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
18463
- import { tmpdir as tmpdir4 } from "os";
18464
- import { spawn as spawn3 } from "child_process";
19311
+ import { basename as basename8, resolve as resolve23 } from "path";
19312
+ import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
19313
+ import { tmpdir as tmpdir5 } from "os";
19314
+ import { spawn as spawn4 } from "child_process";
18465
19315
 
18466
19316
  // src/cli/publish.ts
18467
- import { chmodSync, existsSync as existsSync25, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
19317
+ import { chmodSync, existsSync as existsSync25, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
18468
19318
  import { createHash } from "crypto";
18469
19319
  import { resolve as resolve20 } from "path";
18470
19320
  import { spawnSync as spawnSync2 } from "child_process";
@@ -18604,9 +19454,9 @@ function collectChecks(args2) {
18604
19454
  const gitStatus = args2.runCommand("git", ["status", "--porcelain"], { cwd: args2.rootDir });
18605
19455
  checks.push({
18606
19456
  name: "git-clean",
18607
- ok: gitStatus.status === 0 && gitStatus.stdout.trim() === "",
19457
+ ok: args2.allowDirty || gitStatus.status === 0 && gitStatus.stdout.trim() === "",
18608
19458
  code: "git-clean",
18609
- detail: gitStatus.status !== 0 ? gitStatus.stderr || gitStatus.stdout || "Unable to read git status" : gitStatus.stdout.trim() === "" ? "Working tree is clean." : "Working tree has uncommitted changes."
19459
+ detail: args2.allowDirty ? "Working tree cleanliness check skipped via --allow-dirty." : gitStatus.status !== 0 ? gitStatus.stderr || gitStatus.stdout || "Unable to read git status" : gitStatus.stdout.trim() === "" ? "Working tree is clean." : "Working tree has uncommitted changes."
18610
19460
  });
18611
19461
  if (args2.npmEnabled) {
18612
19462
  checks.push({
@@ -18658,6 +19508,7 @@ function planPublish(config, options = {}) {
18658
19508
  config,
18659
19509
  npmEnabled,
18660
19510
  githubReleaseEnabled,
19511
+ allowDirty: options.allowDirty ?? false,
18661
19512
  packageDir,
18662
19513
  packageName,
18663
19514
  githubRepo,
@@ -18921,7 +19772,7 @@ rm -rf "$INSTALL_DIR"
18921
19772
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
18922
19773
 
18923
19774
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
18924
- echo "If Cursor is already open, restart or reload it so the plugin is picked up."
19775
+ echo "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up."
18925
19776
  `;
18926
19777
  }
18927
19778
  function renderInstallCodexScript(config) {
@@ -19035,7 +19886,7 @@ NODE
19035
19886
 
19036
19887
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
19037
19888
  echo "Updated Codex marketplace catalog at $MARKETPLACE_PATH"
19038
- echo "If Codex is already open, restart it so the plugin is picked up."
19889
+ echo "If Codex is already open, use Plugins > Refresh if that action is available in your current UI, or restart Codex so the plugin is picked up."
19039
19890
  `;
19040
19891
  }
19041
19892
  function renderInstallOpenCodeScript(config) {
@@ -19210,7 +20061,7 @@ function writeChecksumFile(tempRoot, files) {
19210
20061
  const name = filePath.split("/").pop();
19211
20062
  return `${digest} ${name}`;
19212
20063
  }).join("\n");
19213
- writeFileSync5(checksumPath, `${lines}
20064
+ writeFileSync6(checksumPath, `${lines}
19214
20065
  `);
19215
20066
  return checksumPath;
19216
20067
  }
@@ -19249,14 +20100,14 @@ function createReleaseArtifacts(rootDir, config, plan, runCommand) {
19249
20100
  for (const asset of githubRelease.assets) {
19250
20101
  if (asset.kind !== "installer") continue;
19251
20102
  const installerPath = resolve20(tempRoot, asset.name);
19252
- writeFileSync5(installerPath, renderInstallerScript(asset, config, context));
20103
+ writeFileSync6(installerPath, renderInstallerScript(asset, config, context));
19253
20104
  chmodSync(installerPath, 493);
19254
20105
  created.push(installerPath);
19255
20106
  }
19256
20107
  const manifestAsset = githubRelease.assets.find((asset) => asset.kind === "manifest");
19257
20108
  if (manifestAsset) {
19258
20109
  const manifestPath = resolve20(tempRoot, manifestAsset.name);
19259
- writeFileSync5(manifestPath, buildReleaseManifest(config, context));
20110
+ writeFileSync6(manifestPath, buildReleaseManifest(config, context));
19260
20111
  created.push(manifestPath);
19261
20112
  }
19262
20113
  const checksumAsset = githubRelease.assets.find((asset) => asset.kind === "checksum");
@@ -19464,6 +20315,221 @@ function printVerifyInstallResult(result) {
19464
20315
  console.log(result.ok ? "pluxx verify-install passed." : "pluxx verify-install failed.");
19465
20316
  }
19466
20317
 
20318
+ // src/cli/behavioral.ts
20319
+ import { existsSync as existsSync27, readFileSync as readFileSync15 } from "fs";
20320
+ import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
20321
+ import { tmpdir as tmpdir4 } from "os";
20322
+ import { resolve as resolve22 } from "path";
20323
+ import { spawn as spawn3 } from "child_process";
20324
+ var BEHAVIORAL_CONFIG_PATH = ".pluxx/behavioral-smoke.json";
20325
+ var CURSOR_RUNNER_BINARIES2 = ["agent", "cursor-agent"];
20326
+ var SUPPORTED_PLATFORMS = ["claude-code", "cursor", "codex", "opencode"];
20327
+ async function runBehavioralSuite(rootDir, config, targets, options = {}) {
20328
+ const selectedPlatforms = targets.filter(
20329
+ (target) => SUPPORTED_PLATFORMS.includes(target)
20330
+ );
20331
+ const cases = loadBehavioralCases(rootDir, selectedPlatforms, options.promptOverride);
20332
+ const checks = [];
20333
+ for (const behavioralCase of cases) {
20334
+ for (const platform of selectedPlatforms) {
20335
+ const targetConfig = behavioralCase.targets[platform];
20336
+ if (!targetConfig) continue;
20337
+ checks.push(await runBehavioralCheck(rootDir, config, behavioralCase.name, platform, targetConfig));
20338
+ }
20339
+ }
20340
+ return {
20341
+ ok: checks.every((check) => check.ok),
20342
+ source: options.promptOverride ? "--behavioral-prompt" : BEHAVIORAL_CONFIG_PATH,
20343
+ checks
20344
+ };
20345
+ }
20346
+ function loadBehavioralCases(rootDir, targets, promptOverride) {
20347
+ if (promptOverride) {
20348
+ return [{
20349
+ name: "inline-prompt",
20350
+ targets: Object.fromEntries(
20351
+ targets.map((target) => [target, { prompt: promptOverride }])
20352
+ )
20353
+ }];
20354
+ }
20355
+ const filePath = resolve22(rootDir, BEHAVIORAL_CONFIG_PATH);
20356
+ if (!existsSync27(filePath)) {
20357
+ throw new Error(
20358
+ `No behavioral smoke config found at ${BEHAVIORAL_CONFIG_PATH}. Add that file or pass --behavioral-prompt to define a real example query.`
20359
+ );
20360
+ }
20361
+ const parsed = JSON.parse(readFileSync15(filePath, "utf-8"));
20362
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.cases) || parsed.cases.length === 0) {
20363
+ throw new Error(`${BEHAVIORAL_CONFIG_PATH} must contain a non-empty "cases" array.`);
20364
+ }
20365
+ return parsed.cases;
20366
+ }
20367
+ async function runBehavioralCheck(rootDir, config, caseName, platform, targetConfig) {
20368
+ const prompt = targetConfig.prompt.trim();
20369
+ if (!prompt) {
20370
+ throw new Error(`Behavioral smoke case "${caseName}" for ${platform} is missing a prompt.`);
20371
+ }
20372
+ const command2 = await buildBehavioralCommand(platform, prompt, rootDir);
20373
+ const execution = await executeBehavioralCommand(platform, command2, rootDir);
20374
+ const responseText = execution.response.trim();
20375
+ const failures = [];
20376
+ if (execution.exitCode !== 0) {
20377
+ failures.push(`runner exited with code ${execution.exitCode}`);
20378
+ }
20379
+ if (!responseText) {
20380
+ failures.push("runner returned no response text");
20381
+ }
20382
+ for (const required of targetConfig.require ?? []) {
20383
+ if (!includesNeedle(responseText, required)) {
20384
+ failures.push(`missing required text: ${required}`);
20385
+ }
20386
+ }
20387
+ for (const forbidden of targetConfig.forbid ?? []) {
20388
+ if (includesNeedle(responseText, forbidden)) {
20389
+ failures.push(`matched forbidden text: ${forbidden}`);
20390
+ }
20391
+ }
20392
+ return {
20393
+ caseName,
20394
+ platform,
20395
+ prompt,
20396
+ command: command2,
20397
+ ok: failures.length === 0,
20398
+ exitCode: execution.exitCode,
20399
+ responseBytes: responseText.length,
20400
+ responsePreview: truncate2(responseText, 220),
20401
+ require: targetConfig.require,
20402
+ forbid: targetConfig.forbid,
20403
+ failures
20404
+ };
20405
+ }
20406
+ async function buildBehavioralCommand(platform, prompt, workspace) {
20407
+ if (platform === "claude-code") {
20408
+ return [
20409
+ "claude",
20410
+ "--no-session-persistence",
20411
+ "--output-format",
20412
+ "text",
20413
+ "--permission-mode",
20414
+ "acceptEdits",
20415
+ "-p",
20416
+ prompt
20417
+ ];
20418
+ }
20419
+ if (platform === "cursor") {
20420
+ const binary = await resolveCursorBinary2();
20421
+ if (!binary) {
20422
+ throw new Error("Cursor CLI `agent` or `cursor-agent` is not available on PATH.");
20423
+ }
20424
+ await ensureCursorAuthenticated(binary);
20425
+ return [
20426
+ binary,
20427
+ "-p",
20428
+ "--trust",
20429
+ "--workspace",
20430
+ workspace,
20431
+ "--force",
20432
+ prompt
20433
+ ];
20434
+ }
20435
+ if (platform === "codex") {
20436
+ return [
20437
+ "codex",
20438
+ "exec",
20439
+ "--ephemeral",
20440
+ "--skip-git-repo-check",
20441
+ "--full-auto",
20442
+ prompt
20443
+ ];
20444
+ }
20445
+ return ["opencode", "run", prompt];
20446
+ }
20447
+ async function executeBehavioralCommand(platform, command2, cwd) {
20448
+ let codexOutputDir = null;
20449
+ let codexLastMessagePath = null;
20450
+ const runtimeCommand = [...command2];
20451
+ if (platform === "codex") {
20452
+ codexOutputDir = await mkdtemp2(resolve22(tmpdir4(), "pluxx-codex-behavioral-"));
20453
+ codexLastMessagePath = resolve22(codexOutputDir, "last-message.txt");
20454
+ runtimeCommand.splice(2, 0, "--output-last-message", codexLastMessagePath);
20455
+ }
20456
+ try {
20457
+ return await new Promise((resolvePromise, reject) => {
20458
+ const child = spawn3(runtimeCommand[0], runtimeCommand.slice(1), {
20459
+ cwd,
20460
+ stdio: ["ignore", "pipe", "pipe"],
20461
+ env: process.env
20462
+ });
20463
+ const stdoutChunks = [];
20464
+ const stderrChunks = [];
20465
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
20466
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
20467
+ child.on("error", reject);
20468
+ child.on("close", (code) => {
20469
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
20470
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
20471
+ const codexMessage = codexLastMessagePath && existsSync27(codexLastMessagePath) ? readFileSync15(codexLastMessagePath, "utf-8") : "";
20472
+ resolvePromise({
20473
+ exitCode: code ?? 1,
20474
+ response: codexMessage.trim() || stdout.trim() || stderr.trim()
20475
+ });
20476
+ });
20477
+ });
20478
+ } finally {
20479
+ if (codexOutputDir) {
20480
+ await rm3(codexOutputDir, { recursive: true, force: true });
20481
+ }
20482
+ }
20483
+ }
20484
+ async function resolveCursorBinary2() {
20485
+ for (const candidate of CURSOR_RUNNER_BINARIES2) {
20486
+ if (await commandExists2(candidate)) {
20487
+ return candidate;
20488
+ }
20489
+ }
20490
+ return void 0;
20491
+ }
20492
+ async function ensureCursorAuthenticated(binary) {
20493
+ if (process.env.CURSOR_API_KEY && process.env.CURSOR_API_KEY.trim()) {
20494
+ return;
20495
+ }
20496
+ const ok = await commandSucceeds2([binary, "status"]);
20497
+ if (!ok) {
20498
+ throw new Error("Cursor CLI authentication is required. Run `agent login` (or `cursor-agent login`) or export `CURSOR_API_KEY` before behavioral smoke runs.");
20499
+ }
20500
+ }
20501
+ async function commandExists2(binary) {
20502
+ return await new Promise((resolvePromise) => {
20503
+ const child = spawn3("sh", ["-c", `command -v ${shellQuote2(binary)} >/dev/null 2>&1`], {
20504
+ stdio: "ignore",
20505
+ env: process.env
20506
+ });
20507
+ child.on("close", (code) => resolvePromise(code === 0));
20508
+ child.on("error", () => resolvePromise(false));
20509
+ });
20510
+ }
20511
+ async function commandSucceeds2(command2) {
20512
+ return await new Promise((resolvePromise) => {
20513
+ const child = spawn3(command2[0], command2.slice(1), {
20514
+ stdio: "ignore",
20515
+ env: process.env
20516
+ });
20517
+ child.on("close", (code) => resolvePromise(code === 0));
20518
+ child.on("error", () => resolvePromise(false));
20519
+ });
20520
+ }
20521
+ function truncate2(value, length) {
20522
+ if (value.length <= length) return value;
20523
+ return `${value.slice(0, Math.max(0, length - 3))}...`;
20524
+ }
20525
+ function includesNeedle(haystack, needle) {
20526
+ return haystack.toLowerCase().includes(needle.trim().toLowerCase());
20527
+ }
20528
+ function shellQuote2(value) {
20529
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
20530
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20531
+ }
20532
+
19467
20533
  // src/cli/index.ts
19468
20534
  var args = process.argv.slice(2);
19469
20535
  var command = args[0];
@@ -19912,7 +20978,7 @@ function defaultAuthEnvVar(provider, discoveredAuth) {
19912
20978
  function tryOpenBrowser(url) {
19913
20979
  const launcher = process.platform === "darwin" ? { command: "open", args: [url] } : process.platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
19914
20980
  try {
19915
- const child = spawn3(launcher.command, launcher.args, {
20981
+ const child = spawn4(launcher.command, launcher.args, {
19916
20982
  detached: true,
19917
20983
  stdio: "ignore"
19918
20984
  });
@@ -20130,7 +21196,7 @@ async function planInitContextArtifactFiles(rootDir, contextPack) {
20130
21196
  return plannedFiles;
20131
21197
  }
20132
21198
  async function planAuxiliaryFile(rootDir, relativePath, content) {
20133
- const filePath = resolve22(rootDir, relativePath);
21199
+ const filePath = resolve23(rootDir, relativePath);
20134
21200
  const action = await planTextFileAction(filePath, content);
20135
21201
  return {
20136
21202
  relativePath,
@@ -20199,7 +21265,7 @@ async function runInit() {
20199
21265
  if (!runtime.isInteractive) {
20200
21266
  throw new Error("pluxx init requires an interactive terminal unless you use `pluxx init --from-mcp ... --yes`.");
20201
21267
  }
20202
- const dirName = positionalName ? toKebabCase3(positionalName) : basename7(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
21268
+ const dirName = positionalName ? toKebabCase3(positionalName) : basename8(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
20203
21269
  console.log("");
20204
21270
  console.log(" pluxx init \u2014 Create a new plugin");
20205
21271
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -20276,7 +21342,7 @@ ${mcpBlock}${brandBlock}
20276
21342
  targets: [${targetsList}],
20277
21343
  })
20278
21344
  `;
20279
- await writeTextFile(resolve22(process.cwd(), "pluxx.config.ts"), template);
21345
+ await writeTextFile(resolve23(process.cwd(), "pluxx.config.ts"), template);
20280
21346
  const skillDir = `skills/${skillName}`;
20281
21347
  await mkdir4(skillDir, { recursive: true });
20282
21348
  const skillContent = `---
@@ -20298,7 +21364,7 @@ Describe how agents should use this skill.
20298
21364
  Example prompt or command here
20299
21365
  \`\`\`
20300
21366
  `;
20301
- await writeTextFile(resolve22(process.cwd(), `${skillDir}/SKILL.md`), skillContent);
21367
+ await writeTextFile(resolve23(process.cwd(), `${skillDir}/SKILL.md`), skillContent);
20302
21368
  console.log("");
20303
21369
  console.log(" Created:");
20304
21370
  console.log(" pluxx.config.ts");
@@ -20542,7 +21608,7 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
20542
21608
  if (!runtime.dryRun) {
20543
21609
  await applyMcpScaffoldPlan(process.cwd(), plan);
20544
21610
  for (const file of contextArtifactFiles) {
20545
- await writeTextFile(resolve22(process.cwd(), file.relativePath), file.content);
21611
+ await writeTextFile(resolve23(process.cwd(), file.relativePath), file.content);
20546
21612
  }
20547
21613
  }
20548
21614
  const lintResult = runtime.dryRun ? { errors: 0, warnings: 0, issues: [] } : await lintProject(process.cwd());
@@ -20705,7 +21771,7 @@ async function runSync() {
20705
21771
  async function runDoctor() {
20706
21772
  const consumerMode = readFlag(args, "--consumer");
20707
21773
  const doctorPath = args.slice(1).find((value) => !value.startsWith("-"));
20708
- const rootDir = doctorPath ? resolve22(process.cwd(), doctorPath) : process.cwd();
21774
+ const rootDir = doctorPath ? resolve23(process.cwd(), doctorPath) : process.cwd();
20709
21775
  const report = consumerMode ? await doctorConsumer(rootDir) : await doctorProject(rootDir);
20710
21776
  if (runtime.jsonOutput) {
20711
21777
  printJson(report);
@@ -21107,7 +22173,7 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
21107
22173
  review: passDecisions.review,
21108
22174
  verify
21109
22175
  });
21110
- const workspaceRoot = runtime.dryRun ? await mkdtemp2(`${tmpdir4()}/pluxx-autopilot-`) : process.cwd();
22176
+ const workspaceRoot = runtime.dryRun ? await mkdtemp3(`${tmpdir5()}/pluxx-autopilot-`) : process.cwd();
21111
22177
  tempDir = runtime.dryRun ? workspaceRoot : void 0;
21112
22178
  const scaffoldSpinner = createSpinner(runtime);
21113
22179
  scaffoldSpinner?.start(`Autopilot 2/${totalSteps} \xB7 Planning scaffold...`);
@@ -21454,11 +22520,13 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
21454
22520
  }
21455
22521
  } finally {
21456
22522
  if (tempDir) {
21457
- await rm3(tempDir, { recursive: true, force: true });
22523
+ await rm4(tempDir, { recursive: true, force: true });
21458
22524
  }
21459
22525
  }
21460
22526
  }
21461
22527
  async function runTestCommand() {
22528
+ const behavioral = readFlag(args, "--behavioral");
22529
+ const behavioralPrompt = readOption2(args, "--behavioral-prompt");
21462
22530
  const targets = parseTargetFlagValues(args);
21463
22531
  const result = await runTestSuite({
21464
22532
  rootDir: process.cwd(),
@@ -21466,15 +22534,20 @@ async function runTestCommand() {
21466
22534
  });
21467
22535
  const config = result.config.ok ? await loadConfig() : null;
21468
22536
  const platforms = targets ?? config?.targets ?? [];
22537
+ if (behavioral && !args.includes("--install")) {
22538
+ throw new Error("--behavioral requires --install so the selected host CLIs can see the installed plugin bundle.");
22539
+ }
21469
22540
  const install = result.ok && config ? await maybeInstallBuiltOutputs(config, platforms, { verifyConsumers: true }) : void 0;
21470
- const finalResult = install?.verification ? {
22541
+ const behavioralResult = result.ok && config && install && behavioral ? await runBehavioralSuite(process.cwd(), config, platforms, { promptOverride: behavioralPrompt }) : void 0;
22542
+ const finalResult = install?.verification || behavioralResult ? {
21471
22543
  ...result,
21472
- ok: result.ok && install.verification.ok
22544
+ ok: result.ok && (install?.verification?.ok ?? true) && (behavioralResult?.ok ?? true)
21473
22545
  } : result;
21474
22546
  if (runtime.jsonOutput) {
21475
22547
  printJson({
21476
22548
  ...finalResult,
21477
- install
22549
+ install,
22550
+ behavioral: behavioralResult
21478
22551
  });
21479
22552
  return;
21480
22553
  }
@@ -21492,6 +22565,18 @@ async function runTestCommand() {
21492
22565
  console.log(`${prefix} ${check.platform}: ${check.consumerPath}`);
21493
22566
  }
21494
22567
  }
22568
+ if (behavioralResult) {
22569
+ console.log("Behavioral headless smoke:");
22570
+ for (const check of behavioralResult.checks) {
22571
+ const prefix = check.ok ? " PASS" : " FAIL";
22572
+ console.log(`${prefix} ${check.platform}/${check.caseName}: ${check.responsePreview || "(no response preview)"}`);
22573
+ if (!check.ok) {
22574
+ for (const failure of check.failures) {
22575
+ console.log(` - ${failure}`);
22576
+ }
22577
+ }
22578
+ }
22579
+ }
21495
22580
  for (const note of install.notes) {
21496
22581
  console.log(note);
21497
22582
  }
@@ -21598,7 +22683,8 @@ async function runPublishCommand() {
21598
22683
  requestedChannels,
21599
22684
  version: readOption2(args, "--version"),
21600
22685
  tag: readOption2(args, "--tag"),
21601
- dryRun: runtime.dryRun
22686
+ dryRun: runtime.dryRun,
22687
+ allowDirty: args.includes("--allow-dirty")
21602
22688
  });
21603
22689
  if (runtime.dryRun) {
21604
22690
  if (runtime.jsonOutput) {
@@ -21618,7 +22704,8 @@ async function runPublishCommand() {
21618
22704
  requestedChannels,
21619
22705
  version: readOption2(args, "--version"),
21620
22706
  tag: readOption2(args, "--tag"),
21621
- dryRun: false
22707
+ dryRun: false,
22708
+ allowDirty: args.includes("--allow-dirty")
21622
22709
  });
21623
22710
  if (runtime.jsonOutput) {
21624
22711
  printJson(result);
@@ -21641,7 +22728,7 @@ async function runVerifyInstall() {
21641
22728
  const targets = parseTargetFlagValues(args);
21642
22729
  const config = await loadConfig();
21643
22730
  if (runtime.dryRun) {
21644
- const distDir = resolve22(process.cwd(), config.outDir);
22731
+ const distDir = resolve23(process.cwd(), config.outDir);
21645
22732
  const plan = planInstallPlugin(distDir, config.name, targets ?? config.targets);
21646
22733
  const summary = {
21647
22734
  dryRun: true,
@@ -21731,11 +22818,11 @@ Usage:
21731
22818
  pluxx init [name] [--from-mcp <source>] Create a new pluxx.config.ts
21732
22819
  pluxx sync [--from-mcp <source>] Refresh MCP-derived scaffold files
21733
22820
  pluxx migrate <path> Import an existing plugin into pluxx
21734
- pluxx test [--target <platforms...>] [--install] Run config, lint, eval, build, and smoke checks
22821
+ pluxx test [--target <platforms...>] [--install] [--behavioral] Run config, lint, eval, build, and smoke checks
21735
22822
  pluxx eval Evaluate scaffold and prompt-pack quality
21736
22823
  pluxx install [--target <platforms>] [--trust] Install built plugins for local testing
21737
22824
  pluxx verify-install [--target <platforms>] Inspect installed host-visible plugin state
21738
- pluxx publish [--npm] [--github-release] [--dry-run] [--json] [--tag latest] [--version x.y.z]
22825
+ pluxx publish [--npm] [--github-release] [--allow-dirty] [--dry-run] [--json] [--tag latest] [--version x.y.z]
21739
22826
  pluxx uninstall [--target <platforms>] Remove symlinked plugins
21740
22827
  pluxx help Show this help
21741
22828
 
@@ -21744,6 +22831,7 @@ Common flags:
21744
22831
  --quiet Suppress non-error chatter
21745
22832
  --verbose-runner Stream runner stdout/stderr for agent run/autopilot
21746
22833
  --dry-run Show planned work without writing files or installing anything
22834
+ --allow-dirty Skip the clean-working-tree check for publish planning or CI release flows
21747
22835
  --mode quick|standard|thorough Control how much agent refinement autopilot performs
21748
22836
  --approve-mcp-tools Preapprove all tools from the imported MCP in canonical permissions
21749
22837
  --ingest-provider auto|local|firecrawl Choose the docs/website ingestion backend for agent prepare/autopilot
@@ -21792,6 +22880,8 @@ Examples:
21792
22880
  pluxx eval --json Inspect scaffold/prompt-pack quality as JSON
21793
22881
  pluxx test --target claude-code codex Verify selected target outputs
21794
22882
  pluxx test --install Verify and install all configured targets locally
22883
+ pluxx test --install --trust --behavioral Run installed headless example-query smoke checks
22884
+ pluxx test --install --trust --behavioral --behavioral-prompt "Use My Plugin to summarize this repo"
21795
22885
  pluxx install Install to all configured targets
21796
22886
  pluxx install --target claude-code Install to Claude Code only
21797
22887
  pluxx verify-install --target codex Verify the installed Codex bundle in its native local path
@@ -21801,7 +22891,7 @@ Examples:
21801
22891
  }
21802
22892
  if (import.meta.main) {
21803
22893
  main().catch((err) => {
21804
- console.error(err);
22894
+ console.error(err instanceof Error ? err.message : err);
21805
22895
  process.exit(1);
21806
22896
  });
21807
22897
  }