@orchid-labs/pluxx 0.1.4 → 0.1.6

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
@@ -84,6 +84,9 @@ var require_src = __commonJS({
84
84
  }
85
85
  });
86
86
 
87
+ // src/cli/index.ts
88
+ import { readFileSync as readFileSync16 } from "fs";
89
+
87
90
  // src/config/load.ts
88
91
  import { resolve, extname, dirname } from "path";
89
92
  import { existsSync } from "fs";
@@ -4176,27 +4179,67 @@ function mergeAction(current, next) {
4176
4179
  return ACTION_PRIORITY[next] > ACTION_PRIORITY[current] ? next : current;
4177
4180
  }
4178
4181
  function permissionRulesNeedToolLevelDowngrade(permissions) {
4179
- return collectPermissionRules(permissions).some((rule) => rule.pattern !== "*");
4182
+ return collectPermissionRules(permissions).some((rule) => rule.kind === "MCP");
4180
4183
  }
4181
4184
  function buildOpenCodePermissionMap(permissions) {
4182
4185
  const rules = collectPermissionRules(permissions);
4183
4186
  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
4187
  for (const rule of rules) {
4194
- for (const tool of toolAliases[rule.kind]) {
4195
- output[tool] = mergeAction(output[tool], rule.action);
4188
+ if (rule.kind === "MCP") {
4189
+ const toolName = translateCanonicalMcpPermission(rule.pattern);
4190
+ if (!toolName) continue;
4191
+ output[toolName] = mergeScalarPermission(output[toolName], rule.action);
4192
+ continue;
4196
4193
  }
4194
+ const tool = toOpenCodePermissionTool(rule.kind);
4195
+ if (!tool) continue;
4196
+ output[tool] = mergePatternPermission(output[tool], rule.pattern, rule.action);
4197
4197
  }
4198
4198
  return output;
4199
4199
  }
4200
+ function toOpenCodePermissionTool(kind) {
4201
+ switch (kind) {
4202
+ case "Bash":
4203
+ return "bash";
4204
+ case "Edit":
4205
+ return "edit";
4206
+ case "Read":
4207
+ return "read";
4208
+ case "Skill":
4209
+ return "skill";
4210
+ case "MCP":
4211
+ return null;
4212
+ }
4213
+ }
4214
+ function mergeScalarPermission(current, next) {
4215
+ if (!current) return next;
4216
+ if (typeof current === "string") {
4217
+ return mergeAction(current, next);
4218
+ }
4219
+ const merged = { ...current };
4220
+ merged["*"] = mergeAction(merged["*"], next);
4221
+ return merged;
4222
+ }
4223
+ function mergePatternPermission(current, pattern, next) {
4224
+ if (pattern === "*") {
4225
+ return mergeScalarPermission(current, next);
4226
+ }
4227
+ const merged = typeof current === "string" ? { "*": current } : { ...current ?? {} };
4228
+ merged[pattern] = mergeAction(merged[pattern], next);
4229
+ return merged;
4230
+ }
4231
+ function translateCanonicalMcpPermission(pattern) {
4232
+ const trimmed = pattern.trim();
4233
+ if (!trimmed || trimmed === "*") return null;
4234
+ const dot = trimmed.indexOf(".");
4235
+ if (dot === -1) {
4236
+ return `${trimmed}_*`;
4237
+ }
4238
+ const server = trimmed.slice(0, dot).trim();
4239
+ const tool = trimmed.slice(dot + 1).trim();
4240
+ if (!server || !tool) return null;
4241
+ return `${server}_${tool.replace(/\./g, "_")}`;
4242
+ }
4200
4243
  function buildGeneratedPermissionHookScript(permissions) {
4201
4244
  const rules = collectPermissionRules(permissions);
4202
4245
  if (rules.length === 0) return null;
@@ -4334,6 +4377,7 @@ function claudeResponse(match) {
4334
4377
  if (!match) return {};
4335
4378
  return {
4336
4379
  hookSpecificOutput: {
4380
+ hookEventName: "PreToolUse",
4337
4381
  permissionDecision: match.action,
4338
4382
  permissionDecisionReason: "Pluxx permissions matched " + match.rule.raw,
4339
4383
  },
@@ -4726,7 +4770,7 @@ async function loadConfig(dir = process.cwd()) {
4726
4770
  }
4727
4771
 
4728
4772
  // src/generators/index.ts
4729
- import { rmSync, mkdirSync as mkdirSync2 } from "fs";
4773
+ import { rmSync, mkdirSync as mkdirSync3 } from "fs";
4730
4774
  import { resolve as resolve8, relative as relative5 } from "path";
4731
4775
 
4732
4776
  // src/generators/base.ts
@@ -4866,9 +4910,9 @@ var Generator = class {
4866
4910
  for (const configPath of this.config.passthrough ?? []) {
4867
4911
  const src = this.resolveConfigPath(configPath, "passthrough");
4868
4912
  if (!existsSync4(src)) continue;
4869
- const basename8 = src.split("/").filter(Boolean).pop();
4870
- if (!basename8) continue;
4871
- this.copyDir(configPath, `${basename8}/`, "passthrough");
4913
+ const basename9 = src.split("/").filter(Boolean).pop();
4914
+ if (!basename9) continue;
4915
+ this.copyDir(configPath, `${basename9}/`, "passthrough");
4872
4916
  }
4873
4917
  }
4874
4918
  /** Build canonical MCP server configs for target-specific output shaping. */
@@ -4945,8 +4989,8 @@ var Generator = class {
4945
4989
  };
4946
4990
 
4947
4991
  // src/generators/shared/claude-family.ts
4948
- import { existsSync as existsSync5 } from "fs";
4949
- import { resolve as resolve4 } from "path";
4992
+ import { existsSync as existsSync6 } from "fs";
4993
+ import { resolve as resolve5 } from "path";
4950
4994
 
4951
4995
  // src/generators/hooks-warning.ts
4952
4996
  var MATCHER_PASSTHROUGH_PLATFORMS = /* @__PURE__ */ new Set([
@@ -5019,6 +5063,118 @@ function mapHookEventToPascalCase(event) {
5019
5063
  return PASCAL_CASE_HOOK_ALIASES[event] ?? event.charAt(0).toUpperCase() + event.slice(1);
5020
5064
  }
5021
5065
 
5066
+ // src/agents.ts
5067
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
5068
+ import { basename, resolve as resolve4 } from "path";
5069
+ function firstHeading(content) {
5070
+ const lines = content.split(/\r?\n/);
5071
+ for (const line of lines) {
5072
+ const match = line.match(/^#\s+(.*)$/);
5073
+ if (match?.[1]?.trim()) return match[1].trim();
5074
+ }
5075
+ return void 0;
5076
+ }
5077
+ function splitMarkdownFrontmatter(content) {
5078
+ const lines = content.split(/\r?\n/);
5079
+ if (lines[0]?.trim() !== "---") {
5080
+ return {
5081
+ frontmatterLines: [],
5082
+ body: content
5083
+ };
5084
+ }
5085
+ let endIndex = -1;
5086
+ for (let i = 1; i < lines.length; i += 1) {
5087
+ if (lines[i].trim() === "---") {
5088
+ endIndex = i;
5089
+ break;
5090
+ }
5091
+ }
5092
+ if (endIndex === -1) {
5093
+ return {
5094
+ frontmatterLines: [],
5095
+ body: content
5096
+ };
5097
+ }
5098
+ return {
5099
+ frontmatterLines: lines.slice(1, endIndex),
5100
+ body: lines.slice(endIndex + 1).join("\n")
5101
+ };
5102
+ }
5103
+ function parseScalarValue(raw) {
5104
+ const trimmed = raw.trim();
5105
+ if (trimmed === "true") return true;
5106
+ if (trimmed === "false") return false;
5107
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
5108
+ if (trimmed.length >= 2) {
5109
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
5110
+ return trimmed.slice(1, -1);
5111
+ }
5112
+ }
5113
+ return trimmed;
5114
+ }
5115
+ function parseAgentFrontmatter(frontmatterLines) {
5116
+ const root = {};
5117
+ const stack = [
5118
+ { indent: -1, target: root }
5119
+ ];
5120
+ for (const line of frontmatterLines) {
5121
+ if (!line.trim() || line.trim().startsWith("#")) continue;
5122
+ const match = line.match(/^(\s*)(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_.-]+))\s*:\s*(.*)$/);
5123
+ if (!match) continue;
5124
+ const indent = match[1].length;
5125
+ const key = match[2] ?? match[3] ?? match[4];
5126
+ const rawValue = match[5].trim();
5127
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
5128
+ stack.pop();
5129
+ }
5130
+ const parent = stack[stack.length - 1].target;
5131
+ if (!rawValue) {
5132
+ const nested = {};
5133
+ parent[key] = nested;
5134
+ stack.push({ indent, target: nested });
5135
+ continue;
5136
+ }
5137
+ parent[key] = parseScalarValue(rawValue);
5138
+ }
5139
+ return root;
5140
+ }
5141
+ function walkMarkdownFiles(dir) {
5142
+ const entries = readdirSync(dir);
5143
+ const files = [];
5144
+ for (const entry of entries) {
5145
+ const fullPath = resolve4(dir, entry);
5146
+ const stat = statSync(fullPath);
5147
+ if (stat.isDirectory()) {
5148
+ files.push(...walkMarkdownFiles(fullPath));
5149
+ continue;
5150
+ }
5151
+ if (stat.isFile() && entry.endsWith(".md")) {
5152
+ files.push(fullPath);
5153
+ }
5154
+ }
5155
+ return files;
5156
+ }
5157
+ function parseCanonicalAgentFile(agentPath) {
5158
+ const content = readFileSync2(agentPath, "utf-8");
5159
+ const { frontmatterLines, body } = splitMarkdownFrontmatter(content);
5160
+ const frontmatter = parseAgentFrontmatter(frontmatterLines);
5161
+ const fileStem = basename(agentPath, ".md");
5162
+ const name = typeof frontmatter.name === "string" && frontmatter.name ? frontmatter.name : fileStem;
5163
+ const description = typeof frontmatter.description === "string" && frontmatter.description ? frontmatter.description : firstHeading(body);
5164
+ return {
5165
+ filePath: agentPath,
5166
+ fileStem,
5167
+ name,
5168
+ description,
5169
+ body: body.trim(),
5170
+ frontmatter
5171
+ };
5172
+ }
5173
+ function readCanonicalAgentFiles(agentsDir) {
5174
+ if (!agentsDir || !existsSync5(agentsDir)) return [];
5175
+ return walkMarkdownFiles(agentsDir).sort((a, b) => a.localeCompare(b)).map(parseCanonicalAgentFile);
5176
+ }
5177
+
5022
5178
  // src/generators/shared/claude-family.ts
5023
5179
  async function generateClaudeFamilyOutputs(args2) {
5024
5180
  const {
@@ -5030,13 +5186,13 @@ async function generateClaudeFamilyOutputs(args2) {
5030
5186
  writeFile: writeFile3
5031
5187
  } = args2;
5032
5188
  await Promise.all([
5033
- writeManifest(config, options, writeJson),
5189
+ writeManifest(config, rootDir, options, writeJson),
5034
5190
  writeMcpConfig(config, platform, writeJson),
5035
5191
  writeHooks(config, platform, options, writeJson, writeFile3),
5036
5192
  writeInstructions(config, rootDir, options, writeFile3)
5037
5193
  ]);
5038
5194
  }
5039
- async function writeManifest(config, options, writeJson) {
5195
+ async function writeManifest(config, rootDir, options, writeJson) {
5040
5196
  const manifest = {
5041
5197
  name: config.name,
5042
5198
  version: config.version,
@@ -5053,8 +5209,15 @@ async function writeManifest(config, options, writeJson) {
5053
5209
  if (config.commands) {
5054
5210
  manifest.commands = "./commands/";
5055
5211
  }
5056
- if (config.agents) {
5212
+ const agentsManifestMode = options.agentsManifestMode ?? "directory";
5213
+ if (config.agents && agentsManifestMode === "directory") {
5057
5214
  manifest.agents = "./agents/";
5215
+ } else if (config.agents && agentsManifestMode === "files") {
5216
+ const agentsDir = resolve5(rootDir, config.agents);
5217
+ const agents = readCanonicalAgentFiles(agentsDir);
5218
+ if (agents.length > 0) {
5219
+ manifest.agents = agents.map((agent) => `./agents/${agent.fileStem}.md`);
5220
+ }
5058
5221
  }
5059
5222
  manifest.skills = "./skills/";
5060
5223
  if ((config.hooks || config.permissions) && options.includeStandardHooksManifest !== false) {
@@ -5149,8 +5312,8 @@ async function writeHooks(config, platform, options, writeJson, writeFile3) {
5149
5312
  }
5150
5313
  async function writeInstructions(config, rootDir, options, writeFile3) {
5151
5314
  if (!config.instructions) return;
5152
- const srcPath = resolve4(rootDir, config.instructions);
5153
- if (!existsSync5(srcPath)) return;
5315
+ const srcPath = resolve5(rootDir, config.instructions);
5316
+ if (!existsSync6(srcPath)) return;
5154
5317
  const content = await readTextFile(srcPath);
5155
5318
  const titleSuffix = options.titleSuffix ?? "Plugin";
5156
5319
  const instructions = [
@@ -5167,8 +5330,49 @@ function defaultMapEventName(event) {
5167
5330
  }
5168
5331
 
5169
5332
  // 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";
5333
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
5334
+ import { basename as basename2, join as join2 } from "path";
5335
+
5336
+ // src/delegation.ts
5337
+ function getPortableDelegationProfile(frontmatter) {
5338
+ const permission = asMap(frontmatter.permission);
5339
+ const bash = asMap(permission?.bash);
5340
+ const task = asMap(permission?.task);
5341
+ return {
5342
+ mode: typeof frontmatter.mode === "string" ? frontmatter.mode : void 0,
5343
+ hidden: frontmatter.hidden === true,
5344
+ editPolicy: typeof permission?.edit === "string" ? permission.edit : void 0,
5345
+ bashPolicy: typeof bash?.["*"] === "string" ? bash["*"] : void 0,
5346
+ taskPolicy: typeof task?.["*"] === "string" ? task["*"] : void 0
5347
+ };
5348
+ }
5349
+ function buildDelegationBehaviorNotes(frontmatter) {
5350
+ const profile = getPortableDelegationProfile(frontmatter);
5351
+ const notes = [];
5352
+ if (profile.mode === "subagent" || profile.hidden) {
5353
+ notes.push("This specialist is intended primarily for delegated use rather than as the default top-level worker.");
5354
+ }
5355
+ if (profile.editPolicy === "deny") {
5356
+ notes.push("Stay read-only unless the parent task explicitly asks for file edits.");
5357
+ }
5358
+ if (profile.bashPolicy === "deny") {
5359
+ notes.push("Avoid shell commands unless the parent task explicitly requires them.");
5360
+ } else if (profile.bashPolicy === "ask") {
5361
+ notes.push("Use shell commands sparingly and only when they are clearly necessary to complete the task.");
5362
+ }
5363
+ if (profile.taskPolicy === "deny") {
5364
+ notes.push("Do not delegate further subtasks unless the parent task explicitly asks for additional specialist work.");
5365
+ } else if (profile.taskPolicy === "ask") {
5366
+ notes.push("Only delegate further subtasks when the work clearly benefits from another specialist.");
5367
+ }
5368
+ return notes;
5369
+ }
5370
+ function asMap(value) {
5371
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
5372
+ return value;
5373
+ }
5374
+
5375
+ // src/generators/claude-code/index.ts
5172
5376
  var ClaudeCodeGenerator = class extends Generator {
5173
5377
  platform = "claude-code";
5174
5378
  async generate() {
@@ -5180,7 +5384,8 @@ var ClaudeCodeGenerator = class extends Generator {
5180
5384
  manifestPath: ".claude-plugin/plugin.json",
5181
5385
  instructionsFile: "CLAUDE.md",
5182
5386
  pluginRootVar: "CLAUDE_PLUGIN_ROOT",
5183
- includeStandardHooksManifest: false
5387
+ includeStandardHooksManifest: false,
5388
+ agentsManifestMode: "files"
5184
5389
  },
5185
5390
  writeJson: (relativePath, data) => this.writeJson(relativePath, data),
5186
5391
  writeFile: (relativePath, content) => this.writeFile(relativePath, content)
@@ -5195,29 +5400,108 @@ var ClaudeCodeGenerator = class extends Generator {
5195
5400
  copySkills() {
5196
5401
  super.copySkills();
5197
5402
  const collidingSkills = this.collectCollidingSkills();
5403
+ const wrappedSkills = this.collectCommandWrappedSkills();
5198
5404
  for (const skill of collidingSkills) {
5199
5405
  const outputPath = join2(this.outDir, "skills", skill.dirName, "SKILL.md");
5200
- if (!existsSync6(outputPath)) continue;
5201
- const current = readFileSync2(outputPath, "utf-8");
5406
+ if (!existsSync7(outputPath)) continue;
5407
+ const current = readFileSync3(outputPath, "utf-8");
5202
5408
  const hiddenName = buildHiddenSkillName(skill.effectiveName);
5203
- const rewritten = rewriteClaudeCollidingSkill(current, hiddenName);
5409
+ const rewritten = rewriteClaudeSkillVisibility(current, {
5410
+ nameOverride: hiddenName,
5411
+ userInvocable: false
5412
+ });
5413
+ if (rewritten !== current) {
5414
+ writeFileSync(outputPath, rewritten, "utf-8");
5415
+ }
5416
+ }
5417
+ for (const skill of wrappedSkills) {
5418
+ if (collidingSkills.some((entry) => entry.dirName === skill.dirName)) continue;
5419
+ const outputPath = join2(this.outDir, "skills", skill.dirName, "SKILL.md");
5420
+ if (!existsSync7(outputPath)) continue;
5421
+ const current = readFileSync3(outputPath, "utf-8");
5422
+ const rewritten = rewriteClaudeSkillVisibility(current, {
5423
+ userInvocable: false
5424
+ });
5204
5425
  if (rewritten !== current) {
5205
5426
  writeFileSync(outputPath, rewritten, "utf-8");
5206
5427
  }
5207
5428
  }
5208
5429
  }
5430
+ copyAgents() {
5431
+ if (!this.config.agents) return;
5432
+ const agentsDir = this.resolveConfigPath(this.config.agents, "agents");
5433
+ const agents = readCanonicalAgentFiles(agentsDir);
5434
+ if (agents.length === 0) return;
5435
+ mkdirSync2(join2(this.outDir, "agents"), { recursive: true });
5436
+ for (const agent of agents) {
5437
+ const frontmatter = [
5438
+ "---",
5439
+ `name: ${JSON.stringify(agent.name)}`,
5440
+ `description: ${JSON.stringify(agent.description ?? `${agent.name} specialist.`)}`
5441
+ ];
5442
+ if (typeof agent.frontmatter.model === "string" && agent.frontmatter.model) {
5443
+ frontmatter.push(`model: ${JSON.stringify(agent.frontmatter.model)}`);
5444
+ }
5445
+ 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;
5446
+ if (effort) {
5447
+ frontmatter.push(`effort: ${JSON.stringify(effort)}`);
5448
+ }
5449
+ 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;
5450
+ if (typeof maxTurns === "number") {
5451
+ frontmatter.push(`maxTurns: ${maxTurns}`);
5452
+ }
5453
+ const claudeTools = selectClaudeToolsField(agent.frontmatter);
5454
+ if (claudeTools) {
5455
+ frontmatter.push(`tools: ${claudeTools}`);
5456
+ }
5457
+ const disallowedTools = buildClaudeDisallowedTools(agent.frontmatter);
5458
+ if (disallowedTools.length > 0) {
5459
+ frontmatter.push(`disallowedTools: ${disallowedTools.join(", ")}`);
5460
+ }
5461
+ if (typeof agent.frontmatter.skills === "string" && agent.frontmatter.skills.trim()) {
5462
+ frontmatter.push(`skills: ${agent.frontmatter.skills}`);
5463
+ }
5464
+ if (typeof agent.frontmatter.memory === "string" && agent.frontmatter.memory.trim()) {
5465
+ frontmatter.push(`memory: ${JSON.stringify(agent.frontmatter.memory)}`);
5466
+ }
5467
+ if (typeof agent.frontmatter.background === "boolean") {
5468
+ frontmatter.push(`background: ${agent.frontmatter.background}`);
5469
+ }
5470
+ if (typeof agent.frontmatter.isolation === "string" && agent.frontmatter.isolation.trim()) {
5471
+ frontmatter.push(`isolation: ${JSON.stringify(agent.frontmatter.isolation)}`);
5472
+ }
5473
+ if (typeof agent.frontmatter.color === "string" && agent.frontmatter.color.trim()) {
5474
+ frontmatter.push(`color: ${JSON.stringify(agent.frontmatter.color)}`);
5475
+ }
5476
+ frontmatter.push("---");
5477
+ const delegationNotes = buildDelegationBehaviorNotes(agent.frontmatter);
5478
+ const bodyParts = [
5479
+ ...delegationNotes.length > 0 ? [
5480
+ "Delegation contract:",
5481
+ ...delegationNotes.map((note) => `- ${note}`),
5482
+ ""
5483
+ ] : [],
5484
+ agent.body
5485
+ ].filter(Boolean);
5486
+ const outputPath = join2(this.outDir, "agents", `${agent.fileStem}.md`);
5487
+ writeFileSync(outputPath, `${frontmatter.join("\n")}
5488
+
5489
+ ${bodyParts.join("\n").trim()}
5490
+ `, "utf-8");
5491
+ }
5492
+ }
5209
5493
  collectCollidingSkills() {
5210
5494
  if (!this.config.commands) return [];
5211
5495
  const commandsSrc = this.resolveConfigPath(this.config.commands, "commands");
5212
5496
  const skillsSrc = this.resolveConfigPath(this.config.skills, "skills");
5213
- if (!existsSync6(commandsSrc) || !existsSync6(skillsSrc)) return [];
5497
+ if (!existsSync7(commandsSrc) || !existsSync7(skillsSrc)) return [];
5214
5498
  const commandNames = collectTopLevelCommandNames(commandsSrc);
5215
5499
  const collidingSkills = [];
5216
- for (const entry of readdirSync(skillsSrc, { withFileTypes: true })) {
5500
+ for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
5217
5501
  if (!entry.isDirectory()) continue;
5218
5502
  const skillFile = join2(skillsSrc, entry.name, "SKILL.md");
5219
- if (!existsSync6(skillFile)) continue;
5220
- const content = readFileSync2(skillFile, "utf-8");
5503
+ if (!existsSync7(skillFile)) continue;
5504
+ const content = readFileSync3(skillFile, "utf-8");
5221
5505
  const effectiveName = getEffectiveSkillName(content, entry.name);
5222
5506
  if (commandNames.has(effectiveName)) {
5223
5507
  collidingSkills.push({ dirName: entry.name, effectiveName });
@@ -5225,16 +5509,48 @@ var ClaudeCodeGenerator = class extends Generator {
5225
5509
  }
5226
5510
  return collidingSkills;
5227
5511
  }
5512
+ collectCommandWrappedSkills() {
5513
+ if (!this.config.commands) return [];
5514
+ const commandsSrc = this.resolveConfigPath(this.config.commands, "commands");
5515
+ const skillsSrc = this.resolveConfigPath(this.config.skills, "skills");
5516
+ if (!existsSync7(commandsSrc) || !existsSync7(skillsSrc)) return [];
5517
+ const referencedSkills = collectWrappedSkillNames(commandsSrc);
5518
+ if (referencedSkills.size === 0) return [];
5519
+ const wrappedSkills = [];
5520
+ for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
5521
+ if (!entry.isDirectory()) continue;
5522
+ const skillFile = join2(skillsSrc, entry.name, "SKILL.md");
5523
+ if (!existsSync7(skillFile)) continue;
5524
+ const content = readFileSync3(skillFile, "utf-8");
5525
+ const effectiveName = getEffectiveSkillName(content, entry.name);
5526
+ if (referencedSkills.has(effectiveName)) {
5527
+ wrappedSkills.push({ dirName: entry.name, effectiveName });
5528
+ }
5529
+ }
5530
+ return wrappedSkills;
5531
+ }
5228
5532
  };
5229
5533
  function collectTopLevelCommandNames(commandsRoot) {
5230
5534
  const commandNames = /* @__PURE__ */ new Set();
5231
- for (const entry of readdirSync(commandsRoot, { withFileTypes: true })) {
5535
+ for (const entry of readdirSync2(commandsRoot, { withFileTypes: true })) {
5232
5536
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
5233
- commandNames.add(basename(entry.name, ".md"));
5537
+ commandNames.add(basename2(entry.name, ".md"));
5234
5538
  }
5235
5539
  }
5236
5540
  return commandNames;
5237
5541
  }
5542
+ function collectWrappedSkillNames(commandsRoot) {
5543
+ const wrappedSkills = /* @__PURE__ */ new Set();
5544
+ for (const entry of readdirSync2(commandsRoot, { withFileTypes: true })) {
5545
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
5546
+ const content = readFileSync3(join2(commandsRoot, entry.name), "utf-8");
5547
+ for (const match of content.matchAll(/Use the `([^`]+)` skill\./g)) {
5548
+ const skillName = match[1]?.trim();
5549
+ if (skillName) wrappedSkills.add(skillName);
5550
+ }
5551
+ }
5552
+ return wrappedSkills;
5553
+ }
5238
5554
  function getEffectiveSkillName(content, fallback) {
5239
5555
  const frontmatter = extractFrontmatterLines(content);
5240
5556
  if (!frontmatter) return fallback;
@@ -5251,13 +5567,14 @@ function buildHiddenSkillName(name) {
5251
5567
  const trimmed = name.length > maxBaseLength ? name.slice(0, maxBaseLength) : name;
5252
5568
  return `${trimmed}-skill`;
5253
5569
  }
5254
- function rewriteClaudeCollidingSkill(content, hiddenName) {
5570
+ function rewriteClaudeSkillVisibility(content, options) {
5255
5571
  const frontmatter = extractFrontmatterLines(content);
5256
5572
  if (!frontmatter) {
5573
+ const generatedFrontmatter = ["---"];
5574
+ if (options.nameOverride) generatedFrontmatter.push(`name: ${options.nameOverride}`);
5575
+ if (options.userInvocable === false) generatedFrontmatter.push("user-invocable: false");
5257
5576
  return [
5258
- "---",
5259
- `name: ${hiddenName}`,
5260
- "user-invocable: false",
5577
+ ...generatedFrontmatter,
5261
5578
  "---",
5262
5579
  "",
5263
5580
  content.trimStart()
@@ -5268,18 +5585,18 @@ function rewriteClaudeCollidingSkill(content, hiddenName) {
5268
5585
  let sawUserInvocable = false;
5269
5586
  for (let index = 0; index < rewritten.length; index += 1) {
5270
5587
  const trimmed = rewritten[index].trim();
5271
- if (/^name:\s*/i.test(trimmed)) {
5272
- rewritten[index] = `name: ${hiddenName}`;
5588
+ if (options.nameOverride && /^name:\s*/i.test(trimmed)) {
5589
+ rewritten[index] = `name: ${options.nameOverride}`;
5273
5590
  sawName = true;
5274
5591
  continue;
5275
5592
  }
5276
- if (/^user-invocable:\s*/i.test(trimmed)) {
5593
+ if (options.userInvocable === false && /^user-invocable:\s*/i.test(trimmed)) {
5277
5594
  rewritten[index] = "user-invocable: false";
5278
5595
  sawUserInvocable = true;
5279
5596
  }
5280
5597
  }
5281
- if (!sawName) rewritten.push(`name: ${hiddenName}`);
5282
- if (!sawUserInvocable) rewritten.push("user-invocable: false");
5598
+ if (options.nameOverride && !sawName) rewritten.push(`name: ${options.nameOverride}`);
5599
+ if (options.userInvocable === false && !sawUserInvocable) rewritten.push("user-invocable: false");
5283
5600
  const lines = content.split("\n");
5284
5601
  const endIndex = findFrontmatterEndIndex(lines);
5285
5602
  const body = endIndex === -1 ? content : lines.slice(endIndex + 1).join("\n");
@@ -5308,162 +5625,51 @@ function stripYamlScalar(value) {
5308
5625
  }
5309
5626
  return trimmed;
5310
5627
  }
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();
5628
+ function buildClaudeDisallowedTools(frontmatter) {
5629
+ const tools = /* @__PURE__ */ new Set();
5630
+ const permission = asMap2(frontmatter.permission);
5631
+ const bash = asMap2(permission?.bash);
5632
+ const legacyTools = asMap2(frontmatter.tools);
5633
+ if (permission?.edit === "deny") {
5634
+ tools.add("Write");
5635
+ tools.add("Edit");
5636
+ tools.add("MultiEdit");
5323
5637
  }
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
- };
5638
+ if (permission?.bash === "deny" || bash?.["*"] === "deny") {
5639
+ tools.add("Bash");
5333
5640
  }
5334
- let endIndex = -1;
5335
- for (let i = 1; i < lines.length; i += 1) {
5336
- if (lines[i].trim() === "---") {
5337
- endIndex = i;
5338
- break;
5641
+ if (legacyTools?.write === false || legacyTools?.edit === false || legacyTools?.patch === false || legacyTools?.multiedit === false) {
5642
+ tools.add("Write");
5643
+ tools.add("Edit");
5644
+ tools.add("MultiEdit");
5645
+ }
5646
+ if (legacyTools?.bash === false || legacyTools?.shell === false) {
5647
+ tools.add("Bash");
5648
+ }
5649
+ if (typeof frontmatter.disallowedTools === "string") {
5650
+ for (const token of frontmatter.disallowedTools.split(",")) {
5651
+ const trimmed = token.trim();
5652
+ if (trimmed) tools.add(trimmed);
5339
5653
  }
5340
5654
  }
5341
- if (endIndex === -1) {
5342
- return {
5343
- frontmatterLines: [],
5344
- body: content
5345
- };
5655
+ return Array.from(tools);
5656
+ }
5657
+ function selectClaudeToolsField(frontmatter) {
5658
+ if (typeof frontmatter.tools !== "string") return null;
5659
+ const tools = frontmatter.tools.split(",").map((token) => token.trim()).filter(Boolean);
5660
+ if (tools.length === 0) return null;
5661
+ if (tools.some((token) => token.startsWith("mcp__"))) {
5662
+ return null;
5346
5663
  }
5347
- return {
5348
- frontmatterLines: lines.slice(1, endIndex),
5349
- body: lines.slice(endIndex + 1).join("\n")
5350
- };
5664
+ return tools.join(", ");
5351
5665
  }
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
- }
5361
- }
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);
5387
- }
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);
5402
- }
5403
- }
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
- };
5439
- }
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.");
5458
- }
5459
- return notes;
5460
- }
5461
- function asMap(value) {
5666
+ function asMap2(value) {
5462
5667
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
5463
5668
  return value;
5464
5669
  }
5465
5670
 
5466
5671
  // src/generators/cursor/index.ts
5672
+ import { existsSync as existsSync8 } from "fs";
5467
5673
  var CursorGenerator = class extends Generator {
5468
5674
  platform = "cursor";
5469
5675
  async generate() {
@@ -5548,7 +5754,9 @@ var CursorGenerator = class extends Generator {
5548
5754
  if (entry.timeout) hookDef.timeout = entry.timeout;
5549
5755
  if (entry.matcher) hookDef.matcher = entry.matcher;
5550
5756
  if (entry.failClosed) hookDef.failClosed = entry.failClosed;
5551
- if (entry.loop_limit !== void 0) hookDef.loop_limit = entry.loop_limit;
5757
+ if (entry.loop_limit !== void 0 && CURSOR_LOOP_LIMIT_HOOK_EVENTS.includes(event)) {
5758
+ hookDef.loop_limit = entry.loop_limit;
5759
+ }
5552
5760
  return hookDef;
5553
5761
  })
5554
5762
  ];
@@ -5682,6 +5890,23 @@ function parseCommandFrontmatterDescription(frontmatterLines) {
5682
5890
  }
5683
5891
  return void 0;
5684
5892
  }
5893
+ function parseCommandFrontmatterString(frontmatterLines, key) {
5894
+ const pattern = new RegExp(`^${key}:\\s*(.+)\\s*$`, "i");
5895
+ for (const line of frontmatterLines) {
5896
+ const match = pattern.exec(line.trim());
5897
+ if (match?.[1]) {
5898
+ return stripYamlScalar2(match[1]);
5899
+ }
5900
+ }
5901
+ return void 0;
5902
+ }
5903
+ function parseCommandFrontmatterBoolean(frontmatterLines, key) {
5904
+ const value = parseCommandFrontmatterString(frontmatterLines, key);
5905
+ if (!value) return void 0;
5906
+ if (/^true$/i.test(value)) return true;
5907
+ if (/^false$/i.test(value)) return false;
5908
+ return void 0;
5909
+ }
5685
5910
  function walkMarkdownFiles2(dir) {
5686
5911
  const entries = readdirSync3(dir);
5687
5912
  const files = [];
@@ -5712,6 +5937,9 @@ function readCanonicalCommandFiles(commandsDir) {
5712
5937
  commandId,
5713
5938
  title,
5714
5939
  description: parseCommandFrontmatterDescription(frontmatterLines),
5940
+ agent: parseCommandFrontmatterString(frontmatterLines, "agent"),
5941
+ subtask: parseCommandFrontmatterBoolean(frontmatterLines, "subtask"),
5942
+ model: parseCommandFrontmatterString(frontmatterLines, "model"),
5715
5943
  body: body.trim()
5716
5944
  };
5717
5945
  });
@@ -5730,6 +5958,7 @@ var CodexGenerator = class extends Generator {
5730
5958
  async generate() {
5731
5959
  await Promise.all([
5732
5960
  this.generateManifest(),
5961
+ this.generateAppConfig(),
5733
5962
  this.generateMcpConfig(".mcp.json", {
5734
5963
  includeDefaultAuthHeaders: false,
5735
5964
  transformRemoteEntry: ({ name, server }) => {
@@ -5824,6 +6053,11 @@ var CodexGenerator = class extends Generator {
5824
6053
  }
5825
6054
  await this.writeJson(".codex-plugin/plugin.json", manifest);
5826
6055
  }
6056
+ async generateAppConfig() {
6057
+ const appConfig = this.config.platforms?.codex?.app;
6058
+ if (!appConfig || typeof appConfig !== "object" || Array.isArray(appConfig)) return;
6059
+ await this.writeJson(".app.json", appConfig);
6060
+ }
5827
6061
  async generatePermissionsCompanion() {
5828
6062
  const compilerIntent = this.getCompilerIntent();
5829
6063
  const rules = collectPermissionRules(this.config.permissions);
@@ -5961,8 +6195,8 @@ var CodexGenerator = class extends Generator {
5961
6195
  };
5962
6196
 
5963
6197
  // 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";
6198
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
6199
+ import { basename as basename3, resolve as resolve7 } from "path";
5966
6200
  var OpenCodeGenerator = class extends Generator {
5967
6201
  platform = "opencode";
5968
6202
  async generate() {
@@ -5972,9 +6206,11 @@ var OpenCodeGenerator = class extends Generator {
5972
6206
  ]);
5973
6207
  this.copySkills();
5974
6208
  this.copyCommands();
6209
+ this.copyAgents();
5975
6210
  this.copyScripts();
5976
6211
  this.copyAssets();
5977
6212
  this.copyPassthrough();
6213
+ this.rewriteOpenCodeSkillAgentMentions();
5978
6214
  }
5979
6215
  async generatePackageJson() {
5980
6216
  const npmName = this.config.platforms?.opencode?.npmPackage ?? `opencode-${this.config.name}`;
@@ -6286,7 +6522,10 @@ var OpenCodeGenerator = class extends Generator {
6286
6522
  for (const command2 of commands) {
6287
6523
  output[command2.commandId] = {
6288
6524
  template: command2.body,
6289
- ...command2.description ? { description: command2.description } : {}
6525
+ ...command2.description ? { description: command2.description } : {},
6526
+ ...command2.agent ? { agent: command2.agent } : {},
6527
+ ...typeof command2.subtask === "boolean" ? { subtask: command2.subtask } : {},
6528
+ ...command2.model ? { model: command2.model } : {}
6290
6529
  };
6291
6530
  }
6292
6531
  return output;
@@ -6312,14 +6551,36 @@ var OpenCodeGenerator = class extends Generator {
6312
6551
  if (typeof agent.frontmatter.temperature === "number") {
6313
6552
  definition.temperature = agent.frontmatter.temperature;
6314
6553
  }
6554
+ if (typeof agent.frontmatter.steps === "number") {
6555
+ definition.steps = agent.frontmatter.steps;
6556
+ }
6557
+ if (typeof agent.frontmatter.maxSteps === "number" && definition.steps === void 0) {
6558
+ definition.steps = agent.frontmatter.maxSteps;
6559
+ }
6560
+ if (typeof agent.frontmatter.disable === "boolean") {
6561
+ definition.disable = agent.frontmatter.disable;
6562
+ }
6315
6563
  if (typeof agent.frontmatter.hidden === "boolean") {
6316
6564
  definition.hidden = agent.frontmatter.hidden;
6317
6565
  }
6318
- const permission = asOpenCodeMap(agent.frontmatter.permission);
6566
+ if (typeof agent.frontmatter.color === "string" && agent.frontmatter.color) {
6567
+ definition.color = agent.frontmatter.color;
6568
+ }
6569
+ if (typeof agent.frontmatter.topP === "number") {
6570
+ definition.topP = agent.frontmatter.topP;
6571
+ }
6572
+ if (typeof agent.frontmatter.top_p === "number" && definition.topP === void 0) {
6573
+ definition.topP = agent.frontmatter.top_p;
6574
+ }
6575
+ const legacyToolTranslation = translateLegacyOpenCodeTools(agent.frontmatter.tools);
6576
+ const permission = mergeOpenCodeMaps(
6577
+ legacyToolTranslation.permission,
6578
+ asOpenCodeMap(agent.frontmatter.permission)
6579
+ );
6319
6580
  if (permission) {
6320
6581
  definition.permission = permission;
6321
6582
  }
6322
- const tools = asOpenCodeMap(agent.frontmatter.tools);
6583
+ const tools = legacyToolTranslation.untranslated;
6323
6584
  if (tools) {
6324
6585
  definition.tools = tools;
6325
6586
  }
@@ -6401,11 +6662,99 @@ var OpenCodeGenerator = class extends Generator {
6401
6662
  }
6402
6663
  return files;
6403
6664
  }
6665
+ rewriteOpenCodeSkillAgentMentions() {
6666
+ if (!this.config.agents || !this.config.skills) return;
6667
+ const skillsDir = resolve7(this.outDir, "skills");
6668
+ if (!existsSync11(skillsDir)) return;
6669
+ const agentsDir = this.resolveConfigPath(this.config.agents, "agents");
6670
+ const agentNames = readCanonicalAgentFiles(agentsDir).map((agent) => agent.name).filter(Boolean);
6671
+ if (agentNames.length === 0) return;
6672
+ for (const filePath of this.walkFiles(skillsDir)) {
6673
+ if (basename3(filePath) !== "SKILL.md") continue;
6674
+ const source = readFileSync5(filePath, "utf-8");
6675
+ let rewritten = source;
6676
+ for (const agentName of agentNames) {
6677
+ const escaped = escapeRegExp(agentName);
6678
+ rewritten = rewritten.replace(new RegExp(`\`(${escaped})\``, "g"), "`@$1`");
6679
+ }
6680
+ if (rewritten !== source) {
6681
+ writeFileSync2(filePath, rewritten);
6682
+ }
6683
+ }
6684
+ }
6404
6685
  };
6405
6686
  function asOpenCodeMap(value) {
6406
6687
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
6407
6688
  return value;
6408
6689
  }
6690
+ function mergeOpenCodeMaps(base, override) {
6691
+ if (!base) return override;
6692
+ if (!override) return base;
6693
+ const merged = { ...base };
6694
+ for (const [key, value] of Object.entries(override)) {
6695
+ if (isOpenCodeMap(merged[key]) && isOpenCodeMap(value)) {
6696
+ merged[key] = {
6697
+ ...merged[key],
6698
+ ...value
6699
+ };
6700
+ continue;
6701
+ }
6702
+ merged[key] = value;
6703
+ }
6704
+ return merged;
6705
+ }
6706
+ function translateLegacyOpenCodeTools(value) {
6707
+ const tools = asOpenCodeMap(value);
6708
+ if (!tools) return {};
6709
+ const permission = {};
6710
+ const untranslated = {};
6711
+ for (const [rawKey, rawValue] of Object.entries(tools)) {
6712
+ const key = normalizeLegacyOpenCodeToolKey(rawKey);
6713
+ const translated = translateLegacyOpenCodeToolValue(rawValue);
6714
+ if (translated === void 0) {
6715
+ untranslated[rawKey] = rawValue;
6716
+ continue;
6717
+ }
6718
+ permission[key] = translated;
6719
+ }
6720
+ return {
6721
+ ...Object.keys(permission).length > 0 ? { permission } : {},
6722
+ ...Object.keys(untranslated).length > 0 ? { untranslated } : {}
6723
+ };
6724
+ }
6725
+ function normalizeLegacyOpenCodeToolKey(key) {
6726
+ switch (key) {
6727
+ case "write":
6728
+ case "patch":
6729
+ case "multiedit":
6730
+ return "edit";
6731
+ case "shell":
6732
+ return "bash";
6733
+ default:
6734
+ return key;
6735
+ }
6736
+ }
6737
+ function translateLegacyOpenCodeToolValue(value) {
6738
+ if (typeof value === "boolean") {
6739
+ return value ? "allow" : "deny";
6740
+ }
6741
+ if (typeof value === "string" && ["allow", "ask", "deny"].includes(value)) {
6742
+ return value;
6743
+ }
6744
+ if (!isOpenCodeMap(value)) return void 0;
6745
+ const nested = {};
6746
+ for (const [key, rawNested] of Object.entries(value)) {
6747
+ const translated = translateLegacyOpenCodeToolValue(rawNested);
6748
+ if (translated === void 0 || typeof translated === "object") {
6749
+ return void 0;
6750
+ }
6751
+ nested[key] = translated;
6752
+ }
6753
+ return nested;
6754
+ }
6755
+ function isOpenCodeMap(value) {
6756
+ return !!value && typeof value === "object" && !Array.isArray(value);
6757
+ }
6409
6758
  function mapHookEventName(event) {
6410
6759
  const map = {
6411
6760
  sessionStart: "session.created",
@@ -6426,6 +6775,9 @@ function mapHookEventName(event) {
6426
6775
  function toPascalCase(str) {
6427
6776
  return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
6428
6777
  }
6778
+ function escapeRegExp(value) {
6779
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6780
+ }
6429
6781
 
6430
6782
  // src/generators/github-copilot/index.ts
6431
6783
  var GitHubCopilotGenerator = class extends Generator {
@@ -6673,6 +7025,40 @@ var GENERATORS = {
6673
7025
  cline: ClineGenerator,
6674
7026
  amp: AmpGenerator
6675
7027
  };
7028
+ function assertPathWithinRoot(rootDir, configPath, configKey) {
7029
+ const resolvedPath = resolve8(rootDir, configPath);
7030
+ const rel = relative5(rootDir, resolvedPath);
7031
+ if (rel.startsWith("..")) {
7032
+ throw new Error(`${configKey} path "${configPath}" resolves outside the project root.`);
7033
+ }
7034
+ }
7035
+ function validateConfiguredPaths(config, rootDir) {
7036
+ assertPathWithinRoot(rootDir, config.skills, "skills");
7037
+ if (config.commands) {
7038
+ assertPathWithinRoot(rootDir, config.commands, "commands");
7039
+ }
7040
+ if (config.agents) {
7041
+ assertPathWithinRoot(rootDir, config.agents, "agents");
7042
+ }
7043
+ if (config.scripts) {
7044
+ assertPathWithinRoot(rootDir, config.scripts, "scripts");
7045
+ }
7046
+ if (config.assets) {
7047
+ assertPathWithinRoot(rootDir, config.assets, "assets");
7048
+ }
7049
+ if (config.instructions) {
7050
+ assertPathWithinRoot(rootDir, config.instructions, "instructions");
7051
+ }
7052
+ for (const passthroughPath of config.passthrough ?? []) {
7053
+ assertPathWithinRoot(rootDir, passthroughPath, "passthrough");
7054
+ }
7055
+ if (config.brand?.icon) {
7056
+ assertPathWithinRoot(rootDir, config.brand.icon, "brand.icon");
7057
+ }
7058
+ for (const screenshot of config.brand?.screenshots ?? []) {
7059
+ assertPathWithinRoot(rootDir, screenshot, "brand.screenshots");
7060
+ }
7061
+ }
6676
7062
  async function build(config, rootDir, options = {}) {
6677
7063
  const targets = options.targets ?? config.targets;
6678
7064
  const outDir = resolve8(rootDir, config.outDir);
@@ -6682,10 +7068,11 @@ async function build(config, rootDir, options = {}) {
6682
7068
  `outDir "${config.outDir}" resolves outside the project root. Refusing to delete.`
6683
7069
  );
6684
7070
  }
7071
+ validateConfiguredPaths(config, rootDir);
6685
7072
  if (options.clean !== false) {
6686
7073
  rmSync(outDir, { recursive: true, force: true });
6687
7074
  }
6688
- mkdirSync2(outDir, { recursive: true });
7075
+ mkdirSync3(outDir, { recursive: true });
6689
7076
  const generators = targets.map((target) => {
6690
7077
  const GeneratorClass = GENERATORS[target];
6691
7078
  if (!GeneratorClass) {
@@ -6705,7 +7092,7 @@ import { spawn as spawn2 } from "child_process";
6705
7092
 
6706
7093
  // src/cli/lint.ts
6707
7094
  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";
7095
+ import { resolve as resolve9, relative as relative6, basename as basename4, dirname as dirname3 } from "path";
6709
7096
 
6710
7097
  // src/validation/platform-rules.ts
6711
7098
  var STANDARD_SKILL_FRONTMATTER = [
@@ -6813,10 +7200,22 @@ var PLATFORM_LIMIT_POLICIES = {
6813
7200
  },
6814
7201
  "codex": {
6815
7202
  ...NULL_LIMIT_POLICIES,
6816
- skillDescriptionMax: { kind: "hard" },
6817
- skillNameMustMatchDir: { kind: "hard" },
6818
- manifestPromptMax: { kind: "hard" },
6819
- manifestPromptCountMax: { kind: "hard" },
7203
+ skillDescriptionMax: {
7204
+ kind: "advisory",
7205
+ 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."
7206
+ },
7207
+ skillNameMustMatchDir: {
7208
+ kind: "advisory",
7209
+ 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."
7210
+ },
7211
+ manifestPromptMax: {
7212
+ kind: "advisory",
7213
+ 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."
7214
+ },
7215
+ manifestPromptCountMax: {
7216
+ kind: "advisory",
7217
+ 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."
7218
+ },
6820
7219
  manifestPathPrefix: { kind: "hard" },
6821
7220
  instructionsMaxBytes: {
6822
7221
  kind: "hard",
@@ -6869,7 +7268,7 @@ var PLATFORM_LIMIT_POLICIES = {
6869
7268
  var PLATFORM_VALIDATION_RULES = {
6870
7269
  "claude-code": {
6871
7270
  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.",
7271
+ 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
7272
  limits: PLATFORM_LIMITS["claude-code"],
6874
7273
  limitPolicies: PLATFORM_LIMIT_POLICIES["claude-code"],
6875
7274
  skillDiscoveryDirs: [
@@ -6877,7 +7276,21 @@ var PLATFORM_VALIDATION_RULES = {
6877
7276
  ],
6878
7277
  frontmatter: {
6879
7278
  standard: [...STANDARD_SKILL_FRONTMATTER],
6880
- additional: []
7279
+ additional: [
7280
+ "when_to_use",
7281
+ "argument-hint",
7282
+ "arguments",
7283
+ "user-invocable",
7284
+ "allowed-tools",
7285
+ "model",
7286
+ "effort",
7287
+ "context",
7288
+ "agent",
7289
+ "hooks",
7290
+ "paths",
7291
+ "shell"
7292
+ ],
7293
+ notes: "Claude exposes the richest documented skill frontmatter of the core four."
6881
7294
  },
6882
7295
  manifest: {
6883
7296
  files: [".claude-plugin/plugin.json"],
@@ -6885,43 +7298,58 @@ var PLATFORM_VALIDATION_RULES = {
6885
7298
  notes: "The manifest is optional; if present, name is the only required field."
6886
7299
  },
6887
7300
  mcp: {
6888
- files: [".mcp.json"],
7301
+ files: [".mcp.json", ".claude-plugin/plugin.json"],
6889
7302
  rootKey: "mcpServers",
6890
7303
  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."
7304
+ auth: ["headers", "env interpolation", "OAuth 2.0", "bearer tokens", "dynamic headers"],
7305
+ 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
7306
  },
6894
7307
  hooks: {
6895
7308
  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."
7309
+ files: ["hooks/hooks.json", ".claude-plugin/plugin.json", "~/.claude/settings.json", ".claude/settings.json", ".claude/settings.local.json"],
7310
+ eventNames: ["SessionStart", "PreToolUse", "PostToolUse", "PermissionRequest", "TaskCreated", "TaskCompleted", "Stop", "Notification", "ConfigChange"],
7311
+ 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
7312
  },
6900
7313
  instructions: {
6901
7314
  files: ["CLAUDE.md"],
6902
- format: "markdown"
7315
+ format: "markdown",
7316
+ notes: "Claude keeps persistent instructions in CLAUDE.md and pushes longer procedures into skills."
6903
7317
  },
6904
7318
  sources: [
6905
- { label: "Claude Code headless docs", url: "https://code.claude.com/docs/en/headless" },
7319
+ { label: "Claude Code MCP docs", url: "https://code.claude.com/docs/en/mcp" },
7320
+ { label: "Claude Code plugin marketplaces docs", url: "https://code.claude.com/docs/en/plugin-marketplaces" },
7321
+ { label: "Claude Code plugin dependencies docs", url: "https://code.claude.com/docs/en/plugin-dependencies" },
7322
+ { label: "Claude Code features overview", url: "https://code.claude.com/docs/en/features-overview" },
7323
+ { label: "Claude Code best practices", url: "https://code.claude.com/docs/en/best-practices" },
6906
7324
  { label: "Claude Code CLI reference", url: "https://code.claude.com/docs/en/cli-reference" },
6907
7325
  { label: "Claude Code discover plugins docs", url: "https://code.claude.com/docs/en/discover-plugins" },
7326
+ { label: "Claude Code plugins docs", url: "https://code.claude.com/docs/en/plugins" },
6908
7327
  { label: "Claude Code plugins reference", url: "https://code.claude.com/docs/en/plugins-reference" },
7328
+ { label: "Claude Code hooks guide", url: "https://code.claude.com/docs/en/hooks-guide" },
6909
7329
  { 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" }
7330
+ { label: "Claude Code skills docs", url: "https://code.claude.com/docs/en/skills" },
7331
+ { label: "Claude Code sub-agents docs", url: "https://code.claude.com/docs/en/sub-agents" },
7332
+ { label: "Claude Code env vars docs", url: "https://code.claude.com/docs/en/env-vars" }
6911
7333
  ]
6912
7334
  },
6913
7335
  "cursor": {
6914
7336
  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.",
7337
+ 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
7338
  limits: PLATFORM_LIMITS["cursor"],
6917
7339
  limitPolicies: PLATFORM_LIMIT_POLICIES["cursor"],
6918
7340
  skillDiscoveryDirs: [
6919
7341
  { path: "skills/", level: "supported" },
6920
- { path: "SKILL.md", level: "fallback", notes: "Used when no skills directory or manifest skill path is present." }
7342
+ { path: ".cursor/skills/", level: "supported" },
7343
+ { path: "~/.cursor/skills/", level: "supported" },
7344
+ { path: ".agents/skills/", level: "supported" },
7345
+ { path: "~/.agents/skills/", level: "supported" },
7346
+ { path: ".claude/skills/", level: "supported", notes: "Compatibility directory" },
7347
+ { path: ".codex/skills/", level: "supported", notes: "Compatibility directory" }
6921
7348
  ],
6922
7349
  frontmatter: {
6923
7350
  standard: [...STANDARD_SKILL_FRONTMATTER],
6924
- additional: []
7351
+ additional: [],
7352
+ notes: "Cursor skills document the shared frontmatter set plus compatibility metadata and supporting-file patterns."
6925
7353
  },
6926
7354
  manifest: {
6927
7355
  files: [".cursor-plugin/plugin.json"],
@@ -6929,16 +7357,16 @@ var PLATFORM_VALIDATION_RULES = {
6929
7357
  notes: "Cursor documents plugin.json as the required plugin manifest."
6930
7358
  },
6931
7359
  mcp: {
6932
- files: ["mcp.json"],
7360
+ files: ["mcp.json", ".cursor/mcp.json", "~/.cursor/mcp.json"],
6933
7361
  rootKey: "mcpServers",
6934
- transports: ["stdio", "http", "sse"],
6935
- auth: ["headers", "env interpolation"]
7362
+ transports: ["stdio", "sse", "streamable-http"],
7363
+ auth: ["headers", "env interpolation", "OAuth", "static OAuth credentials"]
6936
7364
  },
6937
7365
  hooks: {
6938
7366
  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."
7367
+ files: ["hooks/hooks.json", ".cursor/hooks.json", "~/.cursor/hooks.json"],
7368
+ eventNames: ["sessionStart", "preToolUse", "postToolUse", "subagentStart", "subagentStop", "beforeShellExecution", "afterShellExecution"],
7369
+ notes: "Cursor plugin hooks live under hooks/hooks.json; project and user hooks also exist separately and reload on save."
6942
7370
  },
6943
7371
  instructions: {
6944
7372
  files: ["rules/", "AGENTS.md"],
@@ -6946,25 +7374,32 @@ var PLATFORM_VALIDATION_RULES = {
6946
7374
  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
7375
  },
6948
7376
  sources: [
6949
- { label: "Cursor plugins reference", url: "https://cursor.com/docs/reference/plugins" },
6950
7377
  { label: "Cursor plugins overview", url: "https://cursor.com/docs/plugins" },
6951
7378
  { label: "Cursor hooks docs", url: "https://cursor.com/docs/hooks" },
6952
7379
  { label: "Cursor skills docs", url: "https://cursor.com/docs/skills" },
6953
7380
  { label: "Cursor rules docs", url: "https://cursor.com/docs/rules" },
6954
7381
  { label: "Cursor MCP docs", url: "https://cursor.com/docs/mcp" },
6955
7382
  { label: "Cursor CLI headless docs", url: "https://cursor.com/docs/cli/headless" },
7383
+ { label: "Cursor CLI slash commands", url: "https://cursor.com/docs/cli/reference/slash-commands" },
6956
7384
  { label: "Cursor CLI parameters", url: "https://cursor.com/docs/cli/reference/parameters" },
6957
7385
  { label: "Cursor CLI authentication", url: "https://cursor.com/docs/cli/reference/authentication" },
7386
+ { label: "Cursor CLI permissions", url: "https://cursor.com/docs/cli/reference/permissions" },
7387
+ { label: "Cursor CLI configuration", url: "https://cursor.com/docs/cli/reference/configuration" },
7388
+ { label: "Cursor ACP docs", url: "https://cursor.com/docs/cli/acp" },
6958
7389
  { label: "Cursor subagents docs", url: "https://cursor.com/docs/subagents" }
6959
7390
  ]
6960
7391
  },
6961
7392
  "codex": {
6962
7393
  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.",
7394
+ 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
7395
  limits: PLATFORM_LIMITS["codex"],
6965
7396
  limitPolicies: PLATFORM_LIMIT_POLICIES["codex"],
6966
7397
  skillDiscoveryDirs: [
6967
- { path: "skills/", level: "supported" }
7398
+ { path: "skills/", level: "supported" },
7399
+ { path: "$CWD/.agents/skills/", level: "supported" },
7400
+ { path: "ancestor .agents/skills/", level: "supported", notes: "Walks upward until repo root" },
7401
+ { path: "$HOME/.agents/skills/", level: "supported" },
7402
+ { path: "/etc/codex/skills/", level: "supported" }
6968
7403
  ],
6969
7404
  frontmatter: {
6970
7405
  standard: [...STANDARD_SKILL_FRONTMATTER],
@@ -6976,68 +7411,95 @@ var PLATFORM_VALIDATION_RULES = {
6976
7411
  notes: "The build plugins guide documents plugin.json, skills/, .mcp.json, .app.json, and assets/ as the standard plugin structure."
6977
7412
  },
6978
7413
  mcp: {
6979
- files: [".mcp.json"],
7414
+ files: [".mcp.json", ".codex/config.toml"],
6980
7415
  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."
7416
+ transports: ["stdio", "streamable-http"],
7417
+ auth: ["bearer token", "OAuth", "header env vars"],
7418
+ 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
7419
  },
6985
7420
  hooks: {
6986
7421
  supported: true,
6987
7422
  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."
7423
+ eventNames: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
7424
+ 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
7425
  },
6991
7426
  instructions: {
6992
- files: ["AGENTS.md"],
6993
- format: "markdown"
7427
+ files: ["AGENTS.md", "AGENTS.override.md"],
7428
+ format: "markdown",
7429
+ notes: "Codex also supports model instruction overrides plus configurable fallback filenames for project docs."
6994
7430
  },
6995
7431
  sources: [
7432
+ { label: "Codex plugins docs", url: "https://developers.openai.com/codex/plugins" },
6996
7433
  { label: "Codex build plugins docs", url: "https://developers.openai.com/codex/plugins/build" },
7434
+ { label: "Codex CLI features docs", url: "https://developers.openai.com/codex/cli/features" },
7435
+ { label: "Codex CLI reference docs", url: "https://developers.openai.com/codex/cli/reference" },
7436
+ { label: "Codex slash commands docs", url: "https://developers.openai.com/codex/cli/slash-commands" },
7437
+ { label: "Codex advanced config docs", url: "https://developers.openai.com/codex/config-advanced" },
7438
+ { label: "Codex rules docs", url: "https://developers.openai.com/codex/rules" },
6997
7439
  { label: "Codex hooks docs", url: "https://developers.openai.com/codex/hooks" },
6998
7440
  { label: "Codex skills docs", url: "https://developers.openai.com/codex/skills" },
6999
7441
  { 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" }
7442
+ { label: "Codex AGENTS.md guide", url: "https://developers.openai.com/codex/guides/agents-md" },
7443
+ { label: "Codex subagents docs", url: "https://developers.openai.com/codex/subagents" },
7444
+ { label: "Codex subagents concept docs", url: "https://developers.openai.com/codex/concepts/subagents" },
7445
+ { label: "Codex noninteractive docs", url: "https://developers.openai.com/codex/noninteractive" },
7446
+ { label: "Codex SDK docs", url: "https://developers.openai.com/codex/sdk" },
7447
+ { label: "Codex agents SDK guide", url: "https://developers.openai.com/codex/guides/agents-sdk" }
7001
7448
  ]
7002
7449
  },
7003
7450
  "opencode": {
7004
7451
  platform: "opencode",
7005
- summary: "OpenCode plugins are code-first TypeScript or JavaScript modules that register skills, commands, MCP servers, and hook handlers programmatically.",
7452
+ 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
7453
  limits: PLATFORM_LIMITS["opencode"],
7007
7454
  limitPolicies: PLATFORM_LIMIT_POLICIES["opencode"],
7008
7455
  skillDiscoveryDirs: [
7009
- { path: "skills/", level: "supported" }
7456
+ { path: "skills/", level: "supported" },
7457
+ { path: ".opencode/skills/", level: "supported" },
7458
+ { path: "~/.config/opencode/skills/", level: "supported" },
7459
+ { path: ".claude/skills/", level: "supported", notes: "Compatibility directory" },
7460
+ { path: ".agents/skills/", level: "supported", notes: "Compatibility directory" }
7010
7461
  ],
7011
7462
  frontmatter: {
7012
7463
  standard: [...STANDARD_SKILL_FRONTMATTER],
7013
- additional: []
7464
+ additional: [],
7465
+ notes: "OpenCode supports Agent Skills semantics, but plugin runtime behavior is code-first rather than manifest-first."
7014
7466
  },
7015
7467
  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."
7468
+ files: ["opencode.json", ".opencode/plugins/", "~/.config/opencode/plugins/"],
7469
+ required: false,
7470
+ notes: "OpenCode plugins are loaded as local modules or npm packages through config rather than a dedicated manifest-only bundle."
7019
7471
  },
7020
7472
  mcp: {
7021
- files: ["index.ts"],
7473
+ files: ["opencode.json"],
7474
+ rootKey: "mcp",
7022
7475
  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.'
7476
+ auth: ["headers", "env interpolation", "OAuth"],
7477
+ notes: "OpenCode config owns MCP; plugins can also extend runtime behavior programmatically."
7025
7478
  },
7026
7479
  hooks: {
7027
7480
  supported: true,
7028
- files: ["index.ts"],
7481
+ files: ["plugin module (index.ts/index.js)"],
7029
7482
  eventNames: [],
7030
7483
  notes: "OpenCode hooks are plugin event handlers implemented in code, not a separate hooks.json file."
7031
7484
  },
7032
7485
  instructions: {
7033
- files: ["index.ts"],
7034
- format: "typescript",
7035
- notes: "Plugins inject instructions into the runtime system prompt from code."
7486
+ files: ["AGENTS.md", "CLAUDE.md", "opencode.json"],
7487
+ format: "markdown + json + code",
7488
+ notes: "OpenCode supports AGENTS.md, CLAUDE.md fallback, config instructions, and plugin runtime instruction injection."
7036
7489
  },
7037
7490
  sources: [
7491
+ { label: "OpenCode SDK docs", url: "https://opencode.ai/docs/sdk/" },
7492
+ { label: "OpenCode server docs", url: "https://opencode.ai/docs/server/" },
7493
+ { label: "OpenCode config docs", url: "https://opencode.ai/docs/config/" },
7038
7494
  { label: "OpenCode plugins docs", url: "https://opencode.ai/docs/plugins/" },
7039
7495
  { label: "OpenCode skills docs", url: "https://opencode.ai/docs/skills/" },
7040
- { label: "OpenCode MCP servers docs", url: "https://opencode.ai/docs/mcp-servers/" }
7496
+ { label: "OpenCode commands docs", url: "https://opencode.ai/docs/commands/" },
7497
+ { label: "OpenCode agents docs", url: "https://opencode.ai/docs/agents/" },
7498
+ { label: "OpenCode MCP servers docs", url: "https://opencode.ai/docs/mcp-servers/" },
7499
+ { label: "OpenCode custom tools docs", url: "https://opencode.ai/docs/custom-tools/" },
7500
+ { label: "OpenCode permissions docs", url: "https://opencode.ai/docs/permissions/" },
7501
+ { label: "OpenCode rules docs", url: "https://opencode.ai/docs/rules/" },
7502
+ { label: "OpenCode ACP docs", url: "https://opencode.ai/docs/acp/" }
7041
7503
  ]
7042
7504
  },
7043
7505
  "openhands": {
@@ -7301,7 +7763,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7301
7763
  },
7302
7764
  commands: {
7303
7765
  mode: "preserve",
7304
- nativeSurfaces: ["commands/*.md"]
7766
+ nativeSurfaces: ["commands/*.md", "skills/<skill>/SKILL.md"],
7767
+ notes: "Claude still supports command files, but the product is increasingly converging command workflows into skills."
7305
7768
  },
7306
7769
  agents: {
7307
7770
  mode: "preserve",
@@ -7310,7 +7773,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7310
7773
  },
7311
7774
  hooks: {
7312
7775
  mode: "preserve",
7313
- nativeSurfaces: ["hooks/hooks.json", ".claude-plugin/plugin.json"]
7776
+ nativeSurfaces: ["hooks/hooks.json", ".claude-plugin/plugin.json", "settings hooks", "skill/agent frontmatter hooks"]
7314
7777
  },
7315
7778
  permissions: {
7316
7779
  mode: "translate",
@@ -7323,8 +7786,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7323
7786
  },
7324
7787
  distribution: {
7325
7788
  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."
7789
+ nativeSurfaces: [".claude-plugin/plugin.json", "marketplaces", "install scopes", "user configuration", "/reload-plugins"],
7790
+ notes: "Distribution surfaces are native, including plugin marketplaces and explicit reload behavior."
7328
7791
  }
7329
7792
  },
7330
7793
  sources: PLATFORM_VALIDATION_RULES["claude-code"].sources
@@ -7343,7 +7806,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7343
7806
  },
7344
7807
  commands: {
7345
7808
  mode: "preserve",
7346
- nativeSurfaces: ["commands/*"]
7809
+ nativeSurfaces: ["commands/*", "slash commands"]
7347
7810
  },
7348
7811
  agents: {
7349
7812
  mode: "translate",
@@ -7361,11 +7824,11 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7361
7824
  },
7362
7825
  runtime: {
7363
7826
  mode: "preserve",
7364
- nativeSurfaces: ["mcp.json", ".cursor-plugin/plugin.json", "scripts/", "assets/"]
7827
+ nativeSurfaces: ["mcp.json", ".cursor/mcp.json", "~/.cursor/mcp.json", ".cursor-plugin/plugin.json", "scripts/", "assets/"]
7365
7828
  },
7366
7829
  distribution: {
7367
7830
  mode: "preserve",
7368
- nativeSurfaces: [".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json", "local marketplace install path"]
7831
+ nativeSurfaces: [".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json", "local marketplace install path", "reload window / restart"]
7369
7832
  }
7370
7833
  },
7371
7834
  sources: PLATFORM_VALIDATION_RULES["cursor"].sources
@@ -7408,7 +7871,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7408
7871
  },
7409
7872
  distribution: {
7410
7873
  mode: "preserve",
7411
- nativeSurfaces: [".codex-plugin/plugin.json", "~/.agents/plugins/marketplace.json", "$REPO_ROOT/.agents/plugins/marketplace.json"]
7874
+ nativeSurfaces: [".codex-plugin/plugin.json", "~/.agents/plugins/marketplace.json", "$REPO_ROOT/.agents/plugins/marketplace.json", "cache install path", "restart after update"]
7412
7875
  }
7413
7876
  },
7414
7877
  sources: PLATFORM_VALIDATION_RULES["codex"].sources
@@ -7418,7 +7881,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7418
7881
  buckets: {
7419
7882
  instructions: {
7420
7883
  mode: "translate",
7421
- nativeSurfaces: ["config instructions", "plugin code"],
7884
+ nativeSurfaces: ["AGENTS.md", "CLAUDE.md", "config instructions", "plugin code"],
7422
7885
  notes: "OpenCode instructions are native, but the surface is config- and code-driven rather than manifest markdown only."
7423
7886
  },
7424
7887
  skills: {
@@ -7431,7 +7894,8 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7431
7894
  },
7432
7895
  agents: {
7433
7896
  mode: "preserve",
7434
- nativeSurfaces: ["agents/*.md", "config agent definitions"]
7897
+ nativeSurfaces: ["agents/*.md", "config agent definitions"],
7898
+ 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
7899
  },
7436
7900
  hooks: {
7437
7901
  mode: "translate",
@@ -7440,21 +7904,25 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
7440
7904
  },
7441
7905
  permissions: {
7442
7906
  mode: "preserve",
7443
- nativeSurfaces: ["config permission", "per-agent overrides"]
7907
+ nativeSurfaces: ["config permission", "per-agent overrides"],
7908
+ 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
7909
  },
7445
7910
  runtime: {
7446
7911
  mode: "preserve",
7447
- nativeSurfaces: ["config mcp", "plugin JS/TS runtime", "scripts/", "assets/"]
7912
+ nativeSurfaces: ["opencode.json", "config mcp", "plugin JS/TS runtime", "scripts/", "assets/"]
7448
7913
  },
7449
7914
  distribution: {
7450
7915
  mode: "translate",
7451
- nativeSurfaces: ["local plugin dir", "npm package", "plugin JS/TS entrypoint"],
7916
+ nativeSurfaces: [".opencode/plugins/", "~/.config/opencode/plugins/", "opencode.json", "npm package", "plugin JS/TS entrypoint"],
7452
7917
  notes: "Distribution is native, but there is no single shared manifest analog to Claude, Cursor, or Codex."
7453
7918
  }
7454
7919
  },
7455
7920
  sources: PLATFORM_VALIDATION_RULES["opencode"].sources
7456
7921
  }
7457
7922
  };
7923
+ function getPlatformRules(platform) {
7924
+ return PLATFORM_VALIDATION_RULES[platform];
7925
+ }
7458
7926
  function getCoreFourPrimitiveCapabilities(platform) {
7459
7927
  return CORE_FOUR_PRIMITIVE_CAPABILITIES[platform];
7460
7928
  }
@@ -7520,6 +7988,21 @@ function renderPrimitiveTranslationSummary(summary) {
7520
7988
  lines.push(` ${row.bucket.padEnd(bucketWidth, " ")} ${cells.join(" ")}`);
7521
7989
  }
7522
7990
  lines.push(" legend: keep=preserve xlat=translate weak=degrade drop=drop");
7991
+ const detailLines = [];
7992
+ for (const row of summary.rows) {
7993
+ for (const target of summary.targets) {
7994
+ const mode = row.modes[target];
7995
+ if (!mode || mode === "preserve") continue;
7996
+ const capability = getCoreFourPrimitiveCapabilities(target).buckets[row.bucket];
7997
+ const verb = mode === "translate" ? "re-expressed via" : mode === "degrade" ? "weakened to" : "omitted; nearest surface would be";
7998
+ const suffix = capability.notes ? ` ${capability.notes}` : "";
7999
+ detailLines.push(` - ${row.bucket} on ${TARGET_LABELS[target]}: ${verb} ${capability.nativeSurfaces.join(", ")}.${suffix}`);
8000
+ }
8001
+ }
8002
+ if (detailLines.length > 0) {
8003
+ lines.push(" details:");
8004
+ lines.push(...detailLines);
8005
+ }
7523
8006
  return lines;
7524
8007
  }
7525
8008
 
@@ -7710,16 +8193,27 @@ function lintSkillFile(skillFile, targets, issues, frontmatterCache) {
7710
8193
  platform: "Agent Skills"
7711
8194
  });
7712
8195
  }
7713
- const expectedDirName = basename3(dirname3(skillFile));
8196
+ const expectedDirName = basename4(dirname3(skillFile));
7714
8197
  const platformsRequiringDirMatch = targets.filter((t2) => PLATFORM_LIMITS[t2].skillNameMustMatchDir);
7715
- if (platformsRequiringDirMatch.length > 0 && nameField.value !== expectedDirName) {
7716
- const platformNames = platformsRequiringDirMatch.join(", ");
8198
+ const hardDirMatchPlatforms = platformsRequiringDirMatch.filter((target) => PLATFORM_LIMIT_POLICIES[target].skillNameMustMatchDir.kind === "hard");
8199
+ const advisoryDirMatchPlatforms = platformsRequiringDirMatch.filter((target) => PLATFORM_LIMIT_POLICIES[target].skillNameMustMatchDir.kind !== "hard");
8200
+ if (hardDirMatchPlatforms.length > 0 && nameField.value !== expectedDirName) {
8201
+ const platformNames = hardDirMatchPlatforms.join(", ");
7717
8202
  pushIssue(issues, {
7718
8203
  level: "error",
7719
8204
  code: "skill-name-dir-mismatch",
7720
8205
  message: `Skill name "${nameField.value}" must match directory name "${expectedDirName}" (required by ${platformNames}).`,
7721
8206
  file: skillFile,
7722
- platform: platformsRequiringDirMatch[0]
8207
+ platform: hardDirMatchPlatforms[0]
8208
+ });
8209
+ } else if (advisoryDirMatchPlatforms.length > 0 && nameField.value !== expectedDirName) {
8210
+ const platformNames = advisoryDirMatchPlatforms.join(", ");
8211
+ pushIssue(issues, {
8212
+ level: "warning",
8213
+ code: "skill-name-dir-guideline",
8214
+ message: `Skill name "${nameField.value}" should match directory name "${expectedDirName}" for ${platformNames} compatibility.`,
8215
+ file: skillFile,
8216
+ platform: advisoryDirMatchPlatforms[0]
7723
8217
  });
7724
8218
  }
7725
8219
  if (!nameField.quoted && needsQuotes(nameField.rawValue)) {
@@ -7744,10 +8238,11 @@ function lintSkillFile(skillFile, targets, issues, frontmatterCache) {
7744
8238
  for (const target of targets) {
7745
8239
  const limits = PLATFORM_LIMITS[target];
7746
8240
  if (limits.skillDescriptionMax !== null && descriptionField.value.length > limits.skillDescriptionMax) {
8241
+ const policy = PLATFORM_LIMIT_POLICIES[target].skillDescriptionMax;
7747
8242
  pushIssue(issues, {
7748
- level: "error",
7749
- code: "skill-description-length",
7750
- message: `Description exceeds ${target} max of ${limits.skillDescriptionMax} characters.`,
8243
+ level: policy?.kind === "hard" ? "error" : "warning",
8244
+ code: policy?.kind === "hard" ? "skill-description-length" : "skill-description-guideline",
8245
+ message: policy?.kind === "hard" ? `Description exceeds ${target} max of ${limits.skillDescriptionMax} characters.` : `Description exceeds the Pluxx ${target} compatibility guideline of ${limits.skillDescriptionMax} characters.`,
7751
8246
  file: skillFile,
7752
8247
  platform: target
7753
8248
  });
@@ -7957,6 +8452,42 @@ function lintMcpUrls(config, issues) {
7957
8452
  }
7958
8453
  }
7959
8454
  }
8455
+ function lintMcpRuntimeState(config, issues) {
8456
+ if (!config.mcp) return;
8457
+ const claudeUsesPlatformAuth = config.targets.includes("claude-code") && config.platforms?.["claude-code"]?.mcpAuth === "platform";
8458
+ const cursorUsesPlatformAuth = config.targets.includes("cursor") && config.platforms?.cursor?.mcpAuth === "platform";
8459
+ for (const [serverName, server] of Object.entries(config.mcp)) {
8460
+ if (server.transport === "stdio") {
8461
+ pushIssue(issues, {
8462
+ level: "warning",
8463
+ code: "mcp-stdio-runtime-dependency",
8464
+ message: `MCP server "${serverName}" runs through a local stdio command. End users still need that command and its runtime dependencies available after install.`,
8465
+ file: "pluxx.config.ts",
8466
+ platform: "MCP"
8467
+ });
8468
+ }
8469
+ const runtimeAuthTargets = [];
8470
+ if (server.auth?.type === "platform") {
8471
+ for (const target of config.targets) {
8472
+ if (target === "claude-code" || target === "cursor" || target === "codex" || target === "opencode") {
8473
+ runtimeAuthTargets.push(target);
8474
+ }
8475
+ }
8476
+ } else {
8477
+ if (claudeUsesPlatformAuth) runtimeAuthTargets.push("claude-code");
8478
+ if (cursorUsesPlatformAuth) runtimeAuthTargets.push("cursor");
8479
+ }
8480
+ if (runtimeAuthTargets.length > 0) {
8481
+ pushIssue(issues, {
8482
+ level: "warning",
8483
+ code: "mcp-runtime-auth-external",
8484
+ 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.`,
8485
+ file: "pluxx.config.ts",
8486
+ platform: "MCP"
8487
+ });
8488
+ }
8489
+ }
8490
+ }
7960
8491
  function lintCodexHookCompatibility(config, issues) {
7961
8492
  if (!isCodexTargetEnabled(config) || !config.hooks) return;
7962
8493
  for (const hookEvent of Object.keys(config.hooks)) {
@@ -7979,10 +8510,11 @@ function lintManifestPromptLimits(config, issues) {
7979
8510
  const prompts = config.brand?.defaultPrompts;
7980
8511
  if (!prompts) continue;
7981
8512
  if (limits.manifestPromptCountMax !== null && prompts.length > limits.manifestPromptCountMax) {
8513
+ const policy = PLATFORM_LIMIT_POLICIES[target].manifestPromptCountMax;
7982
8514
  pushIssue(issues, {
7983
- level: "error",
7984
- code: "platform-prompt-count",
7985
- message: `${target} supports at most ${limits.manifestPromptCountMax} default prompts (found ${prompts.length}).`,
8515
+ level: policy?.kind === "hard" ? "error" : "warning",
8516
+ code: policy?.kind === "hard" ? "platform-prompt-count" : "platform-prompt-count-guideline",
8517
+ 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
8518
  file: "pluxx.config.ts",
7987
8519
  platform: target
7988
8520
  });
@@ -7990,10 +8522,11 @@ function lintManifestPromptLimits(config, issues) {
7990
8522
  if (limits.manifestPromptMax !== null) {
7991
8523
  for (const prompt of prompts) {
7992
8524
  if (prompt.length > limits.manifestPromptMax) {
8525
+ const policy = PLATFORM_LIMIT_POLICIES[target].manifestPromptMax;
7993
8526
  pushIssue(issues, {
7994
- level: "error",
7995
- code: "platform-prompt-length",
7996
- message: `A default prompt exceeds ${target} max of ${limits.manifestPromptMax} characters.`,
8527
+ level: policy?.kind === "hard" ? "error" : "warning",
8528
+ code: policy?.kind === "hard" ? "platform-prompt-length" : "platform-prompt-length-guideline",
8529
+ 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
8530
  file: "pluxx.config.ts",
7998
8531
  platform: target
7999
8532
  });
@@ -8183,6 +8716,20 @@ function lintAgentIsolation(agentFiles, issues, frontmatterCache) {
8183
8716
  }
8184
8717
  }
8185
8718
  }
8719
+ function lintOpenCodeAgentFrontmatter(dir, config, issues) {
8720
+ if (!config.targets.includes("opencode") || !config.agents) return;
8721
+ const agents = readCanonicalAgentFiles(resolve9(dir, config.agents));
8722
+ for (const agent of agents) {
8723
+ if (!("tools" in agent.frontmatter)) continue;
8724
+ pushIssue(issues, {
8725
+ level: "warning",
8726
+ code: "opencode-agent-tools-deprecated",
8727
+ 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`.",
8728
+ file: relative6(dir, agent.filePath).replace(/\\/g, "/"),
8729
+ platform: "OpenCode"
8730
+ });
8731
+ }
8732
+ }
8186
8733
  function lintAbsolutePaths(config, issues) {
8187
8734
  const absolutePathPattern = /^\/[a-zA-Z]|^[A-Z]:\\/;
8188
8735
  if (config.hooks) {
@@ -8301,46 +8848,152 @@ function lintCursorHooks(config, issues) {
8301
8848
  if (!CURSOR_SUPPORTED_HOOK_EVENTS.includes(hookEvent)) {
8302
8849
  pushIssue(issues, {
8303
8850
  level: "warning",
8304
- code: "cursor-hook-event-unknown",
8305
- message: `Cursor does not support hook event "${hookEvent}". Supported: ${CURSOR_SUPPORTED_HOOK_EVENTS.join(", ")}`,
8851
+ code: "cursor-hook-event-unknown",
8852
+ message: `Cursor does not support hook event "${hookEvent}". Supported: ${CURSOR_SUPPORTED_HOOK_EVENTS.join(", ")}`,
8853
+ file: "pluxx.config.ts",
8854
+ platform: "Cursor"
8855
+ });
8856
+ }
8857
+ if (!Array.isArray(hookEntries)) continue;
8858
+ for (const entry of hookEntries) {
8859
+ if (!entry || typeof entry !== "object") continue;
8860
+ const rec = entry;
8861
+ if (rec.loop_limit !== void 0 && !CURSOR_LOOP_LIMIT_HOOK_EVENTS.includes(hookEvent)) {
8862
+ pushIssue(issues, {
8863
+ level: "warning",
8864
+ code: "cursor-hook-loop-limit-unsupported-event",
8865
+ message: `Hook "${hookEvent}" has loop_limit but Cursor only supports loop_limit on ${CURSOR_LOOP_LIMIT_HOOK_EVENTS.join(", ")}.`,
8866
+ file: "pluxx.config.ts",
8867
+ platform: "Cursor"
8868
+ });
8869
+ }
8870
+ }
8871
+ }
8872
+ }
8873
+ function lintCursorSkillFrontmatter(config, skillFiles, issues, frontmatterCache) {
8874
+ const supportedByTarget = new Map(
8875
+ ["cursor", "codex", "opencode"].filter((target) => config.targets.includes(target)).map((target) => {
8876
+ const rules = getPlatformRules(target);
8877
+ return [target, /* @__PURE__ */ new Set([...rules.frontmatter.standard, ...rules.frontmatter.additional])];
8878
+ })
8879
+ );
8880
+ if (supportedByTarget.size === 0) return;
8881
+ for (const skillFile of skillFiles) {
8882
+ const { parsed } = getParsedFrontmatterFile(skillFile, frontmatterCache);
8883
+ if (!parsed.valid) continue;
8884
+ for (const [key] of parsed.fields) {
8885
+ for (const [target, supported] of supportedByTarget.entries()) {
8886
+ if (supported.has(key)) continue;
8887
+ const issue = target === "cursor" ? {
8888
+ code: "cursor-skill-frontmatter-unsupported",
8889
+ message: `Skill frontmatter field "${key}" is not supported by Cursor. Supported: ${[...supported].join(", ")}`,
8890
+ platform: "Cursor"
8891
+ } : target === "codex" ? {
8892
+ code: "codex-skill-frontmatter-translation",
8893
+ 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.`,
8894
+ platform: "Codex"
8895
+ } : {
8896
+ code: "opencode-skill-frontmatter-translation",
8897
+ 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.`,
8898
+ platform: "OpenCode"
8899
+ };
8900
+ pushIssue(issues, {
8901
+ level: "warning",
8902
+ file: skillFile,
8903
+ ...issue
8904
+ });
8905
+ }
8906
+ }
8907
+ }
8908
+ }
8909
+ function lintHookFieldTranslations(config, issues) {
8910
+ if (!config.hooks) return;
8911
+ const hasPromptHooks = Object.values(config.hooks).some(
8912
+ (entries) => (entries ?? []).some((entry) => entry.type === "prompt")
8913
+ );
8914
+ const hasFailClosed = Object.values(config.hooks).some(
8915
+ (entries) => (entries ?? []).some((entry) => entry.failClosed !== void 0)
8916
+ );
8917
+ const hasLoopLimit = Object.values(config.hooks).some(
8918
+ (entries) => (entries ?? []).some((entry) => entry.loop_limit !== void 0)
8919
+ );
8920
+ if (hasPromptHooks) {
8921
+ if (config.targets.includes("claude-code")) {
8922
+ pushIssue(issues, {
8923
+ level: "warning",
8924
+ code: "claude-prompt-hook-degrade",
8925
+ 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.",
8926
+ file: "pluxx.config.ts",
8927
+ platform: "claude-code"
8928
+ });
8929
+ }
8930
+ if (config.targets.includes("codex")) {
8931
+ pushIssue(issues, {
8932
+ level: "warning",
8933
+ code: "codex-prompt-hook-drop",
8934
+ message: "Codex currently receives only command-hook companions from Pluxx. Prompt hooks will be dropped from the generated Codex bundle.",
8935
+ file: "pluxx.config.ts",
8936
+ platform: "codex"
8937
+ });
8938
+ }
8939
+ if (config.targets.includes("opencode")) {
8940
+ pushIssue(issues, {
8941
+ level: "warning",
8942
+ code: "opencode-prompt-hook-drop",
8943
+ message: "The current OpenCode runtime wrapper only emits command hooks. Prompt hooks will be dropped from the generated OpenCode plugin.",
8944
+ file: "pluxx.config.ts",
8945
+ platform: "opencode"
8946
+ });
8947
+ }
8948
+ }
8949
+ if (hasFailClosed && config.targets.includes("claude-code")) {
8950
+ pushIssue(issues, {
8951
+ level: "warning",
8952
+ code: "claude-hook-failclosed-degrade",
8953
+ message: "Claude hook entries currently drop `failClosed` in generated output. Keep this behavior host-specific or verify the generated hook bundle carefully.",
8954
+ file: "pluxx.config.ts",
8955
+ platform: "claude-code"
8956
+ });
8957
+ }
8958
+ if (hasLoopLimit) {
8959
+ if (config.targets.includes("claude-code")) {
8960
+ pushIssue(issues, {
8961
+ level: "warning",
8962
+ code: "claude-hook-loop-limit-degrade",
8963
+ message: "Claude outputs currently drop `loop_limit`. Recursive hook protection is not preserved there today.",
8964
+ file: "pluxx.config.ts",
8965
+ platform: "claude-code"
8966
+ });
8967
+ }
8968
+ if (config.targets.includes("codex")) {
8969
+ pushIssue(issues, {
8970
+ level: "warning",
8971
+ code: "codex-hook-loop-limit-drop",
8972
+ message: "Codex hook companions currently drop `loop_limit`. Only command, matcher, timeout, and failClosed survive there today.",
8306
8973
  file: "pluxx.config.ts",
8307
- platform: "Cursor"
8974
+ platform: "codex"
8308
8975
  });
8309
8976
  }
8310
- if (!Array.isArray(hookEntries)) continue;
8311
- for (const entry of hookEntries) {
8312
- if (!entry || typeof entry !== "object") continue;
8313
- const rec = entry;
8314
- if (rec.loop_limit !== void 0 && !CURSOR_LOOP_LIMIT_HOOK_EVENTS.includes(hookEvent)) {
8315
- pushIssue(issues, {
8316
- level: "warning",
8317
- code: "cursor-hook-loop-limit-unsupported-event",
8318
- message: `Hook "${hookEvent}" has loop_limit but Cursor only supports loop_limit on ${CURSOR_LOOP_LIMIT_HOOK_EVENTS.join(", ")}.`,
8319
- file: "pluxx.config.ts",
8320
- platform: "Cursor"
8321
- });
8322
- }
8977
+ if (config.targets.includes("opencode")) {
8978
+ pushIssue(issues, {
8979
+ level: "warning",
8980
+ code: "opencode-hook-loop-limit-drop",
8981
+ message: "OpenCode runtime hooks currently drop `loop_limit`. Recursive hook protection is still Cursor-first in Pluxx.",
8982
+ file: "pluxx.config.ts",
8983
+ platform: "opencode"
8984
+ });
8323
8985
  }
8324
8986
  }
8325
8987
  }
8326
- function lintCursorSkillFrontmatter(config, skillFiles, issues, frontmatterCache) {
8327
- if (!config.targets.includes("cursor")) return;
8328
- const cursorSupportedFrontmatter = ["name", "description", "license", "compatibility", "metadata", "disable-model-invocation"];
8329
- for (const skillFile of skillFiles) {
8330
- const { parsed } = getParsedFrontmatterFile(skillFile, frontmatterCache);
8331
- if (!parsed.valid) continue;
8332
- for (const [key] of parsed.fields) {
8333
- if (!cursorSupportedFrontmatter.includes(key)) {
8334
- pushIssue(issues, {
8335
- level: "warning",
8336
- code: "cursor-skill-frontmatter-unsupported",
8337
- message: `Skill frontmatter field "${key}" is not supported by Cursor. Supported: ${cursorSupportedFrontmatter.join(", ")}`,
8338
- file: skillFile,
8339
- platform: "Cursor"
8340
- });
8341
- }
8342
- }
8343
- }
8988
+ function lintCodexCommandGuidance(config, issues) {
8989
+ if (!config.targets.includes("codex") || !config.commands) return;
8990
+ pushIssue(issues, {
8991
+ level: "warning",
8992
+ code: "codex-commands-routing-guidance",
8993
+ 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.",
8994
+ file: "pluxx.config.ts",
8995
+ platform: "codex"
8996
+ });
8344
8997
  }
8345
8998
  function lintSkillListingBudgets(skillFiles, targets, issues, frontmatterCache) {
8346
8999
  for (const target of targets) {
@@ -8428,12 +9081,12 @@ function lintPermissions(config, issues) {
8428
9081
  });
8429
9082
  }
8430
9083
  if (rules.some((rule) => rule.kind === "Skill")) {
8431
- const nonClaudeTargets = config.targets.filter((target) => target !== "claude-code");
8432
- if (nonClaudeTargets.length > 0) {
9084
+ const limitedTargets = config.targets.filter((target) => !["claude-code", "codex", "opencode"].includes(target));
9085
+ if (limitedTargets.length > 0) {
8433
9086
  pushIssue(issues, {
8434
9087
  level: "warning",
8435
9088
  code: "permissions-skill-selector-limited",
8436
- message: `Skill(...) permission rules are Claude-style and will require downgrade or docs-only handling on ${nonClaudeTargets.join(", ")}.`,
9089
+ message: `Skill(...) permission rules do not have the same native support on ${limitedTargets.join(", ")} and will require downgrade or translation there.`,
8437
9090
  file: "pluxx.config.ts",
8438
9091
  platform: "Permissions"
8439
9092
  });
@@ -8443,7 +9096,7 @@ function lintPermissions(config, issues) {
8443
9096
  pushIssue(issues, {
8444
9097
  level: "warning",
8445
9098
  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.",
9099
+ 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
9100
  file: "pluxx.config.ts",
8448
9101
  platform: "OpenCode"
8449
9102
  });
@@ -8513,11 +9166,13 @@ async function lintProject(dir = process.cwd(), options = {}) {
8513
9166
  lintSettingsJson(dir, issues);
8514
9167
  lintLegacyCommandsDir(dir, lintConfig, issues);
8515
9168
  lintHookEvents(lintConfig, issues);
8516
- const agentsDir = resolve9(dir, "agents");
9169
+ const agentsDir = resolve9(dir, lintConfig.agents ?? "agents");
8517
9170
  const agentFiles = existsSync17(agentsDir) ? collectMarkdownFiles(agentsDir) : [];
8518
9171
  lintAgentFrontmatter(agentFiles, issues, frontmatterCache);
8519
9172
  lintAgentIsolation(agentFiles, issues, frontmatterCache);
9173
+ lintOpenCodeAgentFrontmatter(dir, { ...lintConfig, agents: lintConfig.agents ?? "./agents/" }, issues);
8520
9174
  lintMcpUrls(lintConfig, issues);
9175
+ lintMcpRuntimeState(lintConfig, issues);
8521
9176
  lintBrandMetadata(lintConfig, issues);
8522
9177
  lintCodexOverrides(lintConfig, issues);
8523
9178
  lintCodexHookCompatibility(lintConfig, issues);
@@ -8525,6 +9180,8 @@ async function lintProject(dir = process.cwd(), options = {}) {
8525
9180
  lintCodexHooksExternalConfig(lintConfig, issues);
8526
9181
  lintPermissions(lintConfig, issues);
8527
9182
  lintPrimitiveTranslations(lintConfig, issues);
9183
+ lintHookFieldTranslations(lintConfig, issues);
9184
+ lintCodexCommandGuidance(lintConfig, issues);
8528
9185
  lintCursorHooks(lintConfig, issues);
8529
9186
  lintCursorRuleContentLimits(lintConfig, issues);
8530
9187
  const skillsDir = resolve9(dir, lintConfig.skills);
@@ -8608,7 +9265,7 @@ import { resolve as resolve11 } from "path";
8608
9265
 
8609
9266
  // src/cli/init-from-mcp.ts
8610
9267
  import { mkdir as mkdir2 } from "fs/promises";
8611
- import { basename as basename4, resolve as resolve10 } from "path";
9268
+ import { basename as basename5, resolve as resolve10 } from "path";
8612
9269
 
8613
9270
  // src/user-config.ts
8614
9271
  var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -9188,7 +9845,7 @@ function buildCommandContent(skill, existingContent) {
9188
9845
  const generatedContent = [
9189
9846
  "---",
9190
9847
  `description: ${JSON.stringify(description)}`,
9191
- `argument-hint: ${JSON.stringify(argumentHint)}`,
9848
+ `argument-hint: ${formatArgumentHintFrontmatter(argumentHint)}`,
9192
9849
  "---",
9193
9850
  "",
9194
9851
  entryBlurb,
@@ -9225,6 +9882,14 @@ function buildCommandContent(skill, existingContent) {
9225
9882
  }
9226
9883
  );
9227
9884
  }
9885
+ function formatArgumentHintFrontmatter(value) {
9886
+ const trimmed = value.trim();
9887
+ if (!trimmed) return '""';
9888
+ if (trimmed.includes("\n") || /(^#)|(\s#)/.test(trimmed)) {
9889
+ return JSON.stringify(trimmed);
9890
+ }
9891
+ return trimmed;
9892
+ }
9228
9893
  function buildInstructionsContent(input) {
9229
9894
  const accessLine = describePluginAccess(input.displayName, input.source, input.runtimeAuthMode ?? "inline");
9230
9895
  const lines = [
@@ -10334,7 +10999,7 @@ function derivePluginName(introspection, source) {
10334
10999
  const candidates = [
10335
11000
  introspection.serverInfo.name,
10336
11001
  introspection.serverInfo.title,
10337
- source.transport === "stdio" ? basename4(source.command) : new URL(source.url).hostname.split(".")[0]
11002
+ source.transport === "stdio" ? basename5(source.command) : new URL(source.url).hostname.split(".")[0]
10338
11003
  ].filter((value) => Boolean(value));
10339
11004
  for (const candidate of candidates) {
10340
11005
  const normalized = toKebabCase(candidate);
@@ -10855,7 +11520,7 @@ function printTestResult(result) {
10855
11520
  }
10856
11521
 
10857
11522
  // 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";
11523
+ import { cpSync as cpSync2, existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, readdirSync as readdirSync6, rmdirSync, writeFileSync as writeFileSync3 } from "fs";
10859
11524
  import { dirname as dirname4, isAbsolute, relative as relative7, resolve as resolve13 } from "path";
10860
11525
  import { tmpdir } from "os";
10861
11526
 
@@ -11138,10 +11803,10 @@ async function createSseClient(server) {
11138
11803
  let resolveEndpoint;
11139
11804
  let rejectEndpoint;
11140
11805
  let endpointSettled = false;
11141
- const endpointReady = new Promise((resolve23, reject) => {
11806
+ const endpointReady = new Promise((resolve24, reject) => {
11142
11807
  resolveEndpoint = (value) => {
11143
11808
  endpointSettled = true;
11144
- resolve23(value);
11809
+ resolve24(value);
11145
11810
  };
11146
11811
  rejectEndpoint = (error) => {
11147
11812
  endpointSettled = true;
@@ -11278,7 +11943,7 @@ async function createSseClient(server) {
11278
11943
  async request(method, params) {
11279
11944
  const requestId = nextRequestId();
11280
11945
  const endpoint = endpointUrl ?? await endpointReady;
11281
- const resultPromise = new Promise((resolve23, reject) => {
11946
+ const resultPromise = new Promise((resolve24, reject) => {
11282
11947
  const timeout = setTimeout(() => {
11283
11948
  pending.delete(requestId);
11284
11949
  reject(new McpIntrospectionError(`Timed out waiting for MCP SSE response to ${method}.`));
@@ -11286,7 +11951,7 @@ async function createSseClient(server) {
11286
11951
  pending.set(requestId, {
11287
11952
  resolve: (value) => {
11288
11953
  clearTimeout(timeout);
11289
- resolve23(value);
11954
+ resolve24(value);
11290
11955
  },
11291
11956
  reject: (error) => {
11292
11957
  clearTimeout(timeout);
@@ -11439,7 +12104,7 @@ async function createStdioClient(server) {
11439
12104
  method,
11440
12105
  ...params ? { params } : {}
11441
12106
  });
11442
- return new Promise((resolve23, reject) => {
12107
+ return new Promise((resolve24, reject) => {
11443
12108
  const timeout = setTimeout(() => {
11444
12109
  pending.delete(id);
11445
12110
  reject(new McpIntrospectionError(`Timed out waiting for MCP stdio response to ${method}.`));
@@ -11447,7 +12112,7 @@ async function createStdioClient(server) {
11447
12112
  pending.set(id, {
11448
12113
  resolve: (value) => {
11449
12114
  clearTimeout(timeout);
11450
- resolve23(value);
12115
+ resolve24(value);
11451
12116
  },
11452
12117
  reject: (error) => {
11453
12118
  clearTimeout(timeout);
@@ -11670,7 +12335,7 @@ async function syncFromMcp(options) {
11670
12335
  if (!existsSync20(newSkillPath)) continue;
11671
12336
  const currentContent = readFileSync8(newSkillPath, "utf-8");
11672
12337
  const updatedContent = injectCustomContent(currentContent, extracted.customContent);
11673
- writeFileSync2(newSkillPath, updatedContent, "utf-8");
12338
+ writeFileSync3(newSkillPath, updatedContent, "utf-8");
11674
12339
  }
11675
12340
  const renamedFiles = [];
11676
12341
  const renamedOldDirs = /* @__PURE__ */ new Set();
@@ -11754,7 +12419,7 @@ async function applyPersistedTaxonomy(rootDir) {
11754
12419
  const instructionsPath = "./INSTRUCTIONS.md";
11755
12420
  const previousInstructions = beforeContents.get(instructionsPath);
11756
12421
  if (previousInstructions !== void 0) {
11757
- writeFileSync2(resolveWithinRoot(rootDir, instructionsPath), previousInstructions, "utf-8");
12422
+ writeFileSync3(resolveWithinRoot(rootDir, instructionsPath), previousInstructions, "utf-8");
11758
12423
  }
11759
12424
  const newMetadataPath = resolveWithinRoot(rootDir, MCP_SCAFFOLD_METADATA_PATH);
11760
12425
  const newMetadata = JSON.parse(readFileSync8(newMetadataPath, "utf-8"));
@@ -11779,7 +12444,7 @@ async function applyPersistedTaxonomy(rootDir) {
11779
12444
  }
11780
12445
  for (const file of afterManaged) {
11781
12446
  if (file === instructionsPath && previousInstructions !== void 0) {
11782
- writeFileSync2(resolveWithinRoot(rootDir, file), previousInstructions, "utf-8");
12447
+ writeFileSync3(resolveWithinRoot(rootDir, file), previousInstructions, "utf-8");
11783
12448
  }
11784
12449
  }
11785
12450
  invalidateSavedAgentPack(rootDir);
@@ -11820,7 +12485,7 @@ function preserveCustomContentForRenames(rootDir, renames, pathForName) {
11820
12485
  if (!existsSync20(newPath)) continue;
11821
12486
  const currentContent = readFileSync8(newPath, "utf-8");
11822
12487
  const updatedContent = injectCustomContent(currentContent, extracted.customContent);
11823
- writeFileSync2(newPath, updatedContent, "utf-8");
12488
+ writeFileSync3(newPath, updatedContent, "utf-8");
11824
12489
  }
11825
12490
  }
11826
12491
  function snapshotManagedFiles(rootDir, files) {
@@ -13073,6 +13738,9 @@ function scoreFirecrawlMappedLink(link, kind) {
13073
13738
  const url = link.url.toLowerCase();
13074
13739
  const text = [link.title, link.description, link.url].filter(Boolean).join(" ").toLowerCase();
13075
13740
  let score = 0;
13741
+ if (url.endsWith(".xml") || text.includes("sitemap")) {
13742
+ return -100;
13743
+ }
13076
13744
  if (kind === "docs") {
13077
13745
  if (url.includes("/mcp")) score += 50;
13078
13746
  if (url.includes("quickstart") || url.includes("get-started")) score += 35;
@@ -13221,9 +13889,9 @@ function buildDocsContextArtifact(sources) {
13221
13889
  const remoteSources = sources.filter((source) => source.status === "ok" && (source.kind === "website" || source.kind === "docs"));
13222
13890
  if (remoteSources.length === 0) return void 0;
13223
13891
  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"]);
13892
+ const shortDescription = dedupeRepeatedSentences(remoteSources.map((source) => source.description ?? source.paragraphs?.[0]).find((value) => Boolean(value && value.trim())));
13893
+ const setupHints = collectHintSentences(remoteSources, ["setup", "install", "get started", "quickstart", "configuration", "configuring", "running", "restart", "onlymaincontent", "main content", "npx", "npm", "curl", "remote hosted url"]);
13894
+ const authHints = collectHintSentences(remoteSources, ["auth", "authentication", "api key", "api_key", "firecrawl_api_key", "bearer", "header", "token", "credential"]);
13227
13895
  const warnings = collectHintSentences(remoteSources, ["warning", "note", "requires", "must", "if you", "unavailable", "couldn"]);
13228
13896
  const workflowHints = collectWorkflowHints(remoteSources);
13229
13897
  const importantTerms = collectImportantTerms(remoteSources);
@@ -13297,8 +13965,19 @@ function truncateForNote(value, maxLength) {
13297
13965
  }
13298
13966
  function summarizeMarkdownArtifact(content, metadata = {}) {
13299
13967
  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);
13968
+ 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);
13969
+ const textChunks = cleaned.split(/\n{2,}/).map((chunk) => stripMarkdownFormatting(chunk)).map((chunk) => chunk.replace(/\s+/g, " ").trim()).filter((chunk) => Boolean(chunk) && !chunk.startsWith("#"));
13970
+ const filteredTextChunks = filterLikelyContentText(textChunks);
13971
+ const codeHints = extractMarkdownCodeHints(cleaned);
13972
+ const paragraphCandidates = uniqueStrings([
13973
+ ...filteredTextChunks,
13974
+ ...codeHints
13975
+ ]);
13976
+ const paragraphs = paragraphCandidates.map((chunk, index) => ({
13977
+ chunk,
13978
+ score: scoreMarkdownTextChunk(chunk),
13979
+ index
13980
+ })).sort((left, right) => right.score - left.score || left.index - right.index).map(({ chunk }) => chunk).slice(0, 5);
13302
13981
  const title = metadata.title?.trim() || headings[0] || paragraphs[0];
13303
13982
  const description = metadata.description?.trim() || paragraphs[0];
13304
13983
  const lines = [];
@@ -13325,6 +14004,42 @@ function summarizeMarkdownArtifact(content, metadata = {}) {
13325
14004
  function stripMarkdownFormatting(value) {
13326
14005
  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
14006
  }
14007
+ function extractMarkdownCodeHints(content) {
14008
+ const hints = [];
14009
+ for (const match of content.matchAll(/```[^\n]*\n([\s\S]*?)```/g)) {
14010
+ const block = match[1] ?? "";
14011
+ for (const rawLine of block.split("\n")) {
14012
+ const line = rawLine.replace(/\r/g, "").trim();
14013
+ if (!line) continue;
14014
+ 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)) {
14015
+ continue;
14016
+ }
14017
+ const normalized = line.replace(/^[`"'[{(]+|[`"'[\]}):,;]+$/g, "").replace(/\s+/g, " ").trim();
14018
+ if (normalized) {
14019
+ hints.push(normalized);
14020
+ }
14021
+ }
14022
+ }
14023
+ return uniqueStrings(hints).slice(0, 10);
14024
+ }
14025
+ function scoreMarkdownTextChunk(value) {
14026
+ const normalized = value.replace(/\s+/g, " ").trim();
14027
+ if (!normalized) return Number.NEGATIVE_INFINITY;
14028
+ if (isLikelyChromeText(normalized)) return -200;
14029
+ const lower = normalized.toLowerCase();
14030
+ let score = 0;
14031
+ if (/[.!?:]/.test(normalized)) score += 20;
14032
+ if (/`|https?:\/\/|(^| )npx( |$)|(^| )npm( |$)|(^| )curl( |$)|=/.test(normalized)) score += 20;
14033
+ 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")) {
14034
+ score += 25;
14035
+ }
14036
+ 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")) {
14037
+ score += 25;
14038
+ }
14039
+ if (normalized.length >= 24 && normalized.length <= 240) score += 10;
14040
+ if (normalized.split(/\s+/).length > 40) score -= 10;
14041
+ return score;
14042
+ }
13328
14043
  function summarizeHtml(html) {
13329
14044
  const cleanedHtml = stripNonContentHtml(html);
13330
14045
  const primaryHtml = selectPrimaryHtmlFragment(cleanedHtml);
@@ -13394,14 +14109,38 @@ function filterLikelyContentText(values) {
13394
14109
  }
13395
14110
  return uniqueStrings(values).map((value) => value.replace(/\s+/g, " ").trim()).filter((value) => value.length > 0);
13396
14111
  }
14112
+ function dedupeRepeatedSentences(value) {
14113
+ if (!value) return void 0;
14114
+ const normalized = value.replace(/\s+/g, " ").replace(/([.!?])\s*,\s*/g, "$1 ").trim();
14115
+ if (!normalized) return void 0;
14116
+ const sentenceMatches = normalized.match(/[^.!?]+[.!?]?/g);
14117
+ if (!sentenceMatches) return normalized;
14118
+ const seen = /* @__PURE__ */ new Set();
14119
+ const uniqueSentences = [];
14120
+ for (const rawSentence of sentenceMatches) {
14121
+ const sentence = rawSentence.trim().replace(/^,+\s*/, "");
14122
+ if (!sentence) continue;
14123
+ const key = sentence.replace(/[.!?]+$/, "").replace(/\s+/g, " ").trim().toLowerCase();
14124
+ if (!key || seen.has(key)) continue;
14125
+ seen.add(key);
14126
+ uniqueSentences.push(sentence);
14127
+ }
14128
+ if (uniqueSentences.length === 0) {
14129
+ return normalized;
14130
+ }
14131
+ return uniqueSentences.join(" ");
14132
+ }
13397
14133
  function isLikelyChromeHeading(value) {
13398
14134
  const normalized = value.replace(/\s+/g, " ").trim();
13399
14135
  if (!normalized) return true;
13400
14136
  const lower = normalized.toLowerCase();
13401
14137
  const chromePatterns = [
13402
14138
  "search docs",
14139
+ "skip to main content",
13403
14140
  "table of contents",
13404
14141
  "on this page",
14142
+ "navigation",
14143
+ "ctrl k",
13405
14144
  "previous",
13406
14145
  "next",
13407
14146
  "privacy",
@@ -13411,6 +14150,8 @@ function isLikelyChromeHeading(value) {
13411
14150
  "blog",
13412
14151
  "pricing",
13413
14152
  "careers",
14153
+ "playground",
14154
+ "community",
13414
14155
  "discord",
13415
14156
  "github",
13416
14157
  "twitter",
@@ -13426,8 +14167,11 @@ function isLikelyChromeText(value) {
13426
14167
  const wordCount = normalized.split(/\s+/).length;
13427
14168
  const chromePatterns = [
13428
14169
  "search docs",
14170
+ "skip to main content",
13429
14171
  "table of contents",
13430
14172
  "on this page",
14173
+ "navigation",
14174
+ "ctrl k",
13431
14175
  "previous",
13432
14176
  "next",
13433
14177
  "privacy",
@@ -13437,6 +14181,8 @@ function isLikelyChromeText(value) {
13437
14181
  "blog",
13438
14182
  "pricing",
13439
14183
  "careers",
14184
+ "playground",
14185
+ "community",
13440
14186
  "discord",
13441
14187
  "github",
13442
14188
  "twitter",
@@ -13563,11 +14309,11 @@ function normalizeProductNameCandidate(value) {
13563
14309
  function collectHintSentences(sources, keywords) {
13564
14310
  const sentences = uniqueStrings(
13565
14311
  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);
14312
+ const segments = [
14313
+ ...source.description ? splitIntoHintSegments(source.description) : [],
14314
+ ...(source.paragraphs ?? []).flatMap(splitIntoHintSegments)
14315
+ ];
14316
+ return segments.filter((sentence) => keywords.some((keyword) => sentence.toLowerCase().includes(keyword))).slice(0, 6);
13571
14317
  })
13572
14318
  );
13573
14319
  return sentences.slice(0, 6);
@@ -13579,16 +14325,50 @@ function collectWorkflowHints(sources) {
13579
14325
  "manual installation",
13580
14326
  "configuration",
13581
14327
  "environment variables",
14328
+ "remote hosted url",
14329
+ "available tools",
13582
14330
  "features",
13583
14331
  "get started",
13584
14332
  "overview",
13585
14333
  "developer guides",
13586
14334
  "quickstarts",
13587
- "mcp server"
14335
+ "mcp server",
14336
+ "ready to build?",
14337
+ "ready to build",
14338
+ "use well-known tools",
14339
+ "code you can trust",
14340
+ "frequently asked questions"
13588
14341
  ]);
13589
- return uniqueStrings(
14342
+ const workflowKeywords = ["search", "scrape", "map", "crawl", "extract", "agent", "browser", "workflow", "knowledge"];
14343
+ const workflowStopKeywords = ["install", "auth", "token", "header", "configuration", "quickstart"];
14344
+ const sourceTitles = new Set(
14345
+ sources.flatMap((source) => source.title ? [source.title.toLowerCase()] : []).filter(Boolean)
14346
+ );
14347
+ const headingHints = uniqueStrings(
13590
14348
  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);
14349
+ ).map(normalizeHintText).filter(
14350
+ (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("://")
14351
+ ).map((heading, index) => ({
14352
+ value: heading,
14353
+ index,
14354
+ score: scoreWorkflowHint(heading, workflowKeywords)
14355
+ })).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).map((entry) => entry.value);
14356
+ const paragraphHints = uniqueStrings(
14357
+ sources.flatMap(
14358
+ (source) => (source.paragraphs ?? []).flatMap(splitIntoHintSegments).filter((segment) => {
14359
+ const lower = segment.toLowerCase();
14360
+ return workflowKeywords.some((keyword) => containsWholeWordKeyword(lower, keyword)) && !workflowStopKeywords.some((keyword) => lower.includes(keyword)) && !segment.includes("://") && !segment.includes("=");
14361
+ })
14362
+ )
14363
+ ).map(normalizeHintText).map((segment, index) => ({
14364
+ value: segment,
14365
+ index,
14366
+ score: scoreWorkflowHint(segment, workflowKeywords)
14367
+ })).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).map((entry) => entry.value);
14368
+ return uniqueStrings([
14369
+ ...headingHints,
14370
+ ...paragraphHints
14371
+ ]).slice(0, 8);
13592
14372
  }
13593
14373
  function collectImportantTerms(sources) {
13594
14374
  return uniqueStrings(
@@ -13598,8 +14378,34 @@ function collectImportantTerms(sources) {
13598
14378
  ])
13599
14379
  ).map((term) => term.replace(/\s+/g, " ").trim()).filter((term) => term.length > 0 && term.split(/\s+/).length <= 4).slice(0, 8);
13600
14380
  }
13601
- function splitIntoSentences(value) {
13602
- return value.split(/(?<=[.!?])\s+/).map((sentence) => sentence.replace(/\s+/g, " ").trim()).filter(Boolean);
14381
+ function splitIntoHintSegments(value) {
14382
+ return value.split(/(?<=[.!?])\s+|\n+/).map(normalizeHintText).filter(Boolean);
14383
+ }
14384
+ function normalizeHintText(value) {
14385
+ 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();
14386
+ }
14387
+ function scoreWorkflowHint(value, workflowKeywords) {
14388
+ const normalized = normalizeHintText(value);
14389
+ const lower = normalized.toLowerCase();
14390
+ let score = 0;
14391
+ for (const keyword of workflowKeywords) {
14392
+ if (containsWholeWordKeyword(lower, keyword)) {
14393
+ score += 30;
14394
+ } else if (lower.includes(keyword)) {
14395
+ score += 10;
14396
+ }
14397
+ }
14398
+ if (lower.includes("feature") || lower.includes("ready to build") || lower.includes("faq") || lower.includes("question")) {
14399
+ score -= 20;
14400
+ }
14401
+ if (normalized.split(/\s+/).length <= 3) {
14402
+ score += 5;
14403
+ }
14404
+ return score;
14405
+ }
14406
+ function containsWholeWordKeyword(value, keyword) {
14407
+ const pattern = new RegExp(`\\b${keyword.replace(/\s+/g, "\\s+")}\\b`, "i");
14408
+ return pattern.test(value);
13603
14409
  }
13604
14410
  function uniqueStrings(values) {
13605
14411
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
@@ -13656,17 +14462,22 @@ function buildAgentPrompt(kind, input) {
13656
14462
  2. Infer the MCP's real product surfaces and workflows from tools, resources, resource templates, and prompt templates.
13657
14463
  3. Merge, split, or rename generated skills so labels are product-facing, not lexical buckets.
13658
14464
  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.
14465
+ 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.
14466
+ 6. Promote specialist, delegated, reviewer, or bounded-execution workflows into agents/subagents when isolation is a better native fit than another inline skill.
14467
+ 7. Keep setup/onboarding, account-admin, and runtime workflows intentionally separated when appropriate.
14468
+ 8. Eliminate misleading labels such as contact or people discovery when the tools do not actually perform direct lookup.
14469
+ 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.
14470
+ 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.
14471
+ 11. Reject stale scaffold assumptions; if current files conflict with discovery context, prefer the discovery evidence and flag the mismatch.
14472
+ 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
14473
  ${buildPromptOverrideBlock(kind, input.overrides)}
13664
14474
  Success criteria:
13665
14475
  - each skill represents a real user workflow or product surface
13666
14476
  - skill names are product-shaped and avoid raw MCP tool/server identifiers when possible
13667
14477
  - setup/onboarding, account-admin, and runtime workflows are grouped intentionally
13668
14478
  - 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
14479
+ - commands stay aligned with the chosen taxonomy, avoid weak command UX, and use realistic arguments when workflows are parameterized
14480
+ - specialist or delegated workflows are promoted into agents/subagents when that native shape is stronger than another flat skill
13670
14481
  - per-skill resource and prompt-template associations remain coherent with the chosen taxonomy
13671
14482
  - taxonomy decisions are grounded in current discovery context, not stale scaffold assumptions
13672
14483
  `;
@@ -13679,7 +14490,9 @@ Success criteria:
13679
14490
  4. Keep wording aligned to the MCP's product narrative and branded language; avoid raw MCP server/tool identifiers except when technically required.
13680
14491
  5. Prefer the branded product name in user-facing copy; do not lead with internal MCP server identifiers.
13681
14492
  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.
14493
+ 7. When discovery implies a parameterized workflow, make the examples show realistic arguments instead of bare placeholder commands.
14494
+ 8. Call out when a request should route to a specialist agent/subagent instead of the generic skill path.
14495
+ 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
14496
  ${buildPromptOverrideBlock(kind, input.overrides)}
13684
14497
  Success criteria:
13685
14498
  - instructions are concise, actionable, and product-shaped
@@ -13688,20 +14501,21 @@ Success criteria:
13688
14501
  - raw MCP server identifiers are omitted unless operationally necessary
13689
14502
  - the generated section reads like routing guidance, not pasted vendor docs
13690
14503
  - command examples use strong command UX (clear intent, realistic args, and runnable shapes)
14504
+ - specialist routing is explicit when certain work should go to an agent/subagent instead of a generic skill
13691
14505
  - workflow guidance stays coherent with related resource and prompt-template evidence in the context
13692
14506
  - the file remains safe for future \`pluxx sync --from-mcp\`
13693
14507
  `;
13694
14508
  }
13695
14509
  return `${sharedIntro.join("\n")}Your job:
13696
14510
  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"}.
14511
+ 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
14512
  3. Separate scaffold quality findings from runtime-correctness findings.
13699
14513
  4. Propose only the highest-value changes needed to make the scaffold useful.
13700
14514
  ${buildPromptOverrideBlock(kind, input.overrides)}
13701
14515
  Success criteria:
13702
14516
  - findings are concrete and tied to files
13703
14517
  - 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
14518
+ - 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
14519
  - suggested changes improve user-facing plugin quality
13706
14520
  - recommendations stay inside Pluxx-managed boundaries
13707
14521
  `;
@@ -14306,11 +15120,11 @@ ${additions.map((block) => `- ${block.replace(/\n/g, "\n ")}`).join("\n")}
14306
15120
 
14307
15121
  // src/cli/doctor.ts
14308
15122
  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";
15123
+ import { basename as basename6, dirname as dirname6, resolve as resolve16 } from "path";
14310
15124
 
14311
15125
  // src/cli/install.ts
14312
15126
  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";
15127
+ 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
15128
  import { spawnSync } from "child_process";
14315
15129
  import * as readline2 from "readline";
14316
15130
  function listHookCommands(hooks) {
@@ -14476,7 +15290,7 @@ function getInstallTargets(pluginName) {
14476
15290
  {
14477
15291
  platform: "opencode",
14478
15292
  pluginDir: resolve15(home, ".config/opencode/plugins", pluginName),
14479
- description: `~/.config/opencode/plugins/${pluginName}.ts`
15293
+ description: `~/.config/opencode/plugins/${pluginName}.ts + ~/.config/opencode/plugins/${pluginName}/`
14480
15294
  },
14481
15295
  {
14482
15296
  platform: "github-copilot",
@@ -14524,7 +15338,7 @@ function toPascalCase2(value) {
14524
15338
  function writeOpenCodeEntryFile(pluginDir, pluginName) {
14525
15339
  const entryPath = getOpenCodeEntryPath(pluginDir);
14526
15340
  const exportName = toPascalCase2(pluginName);
14527
- writeFileSync3(
15341
+ writeFileSync4(
14528
15342
  entryPath,
14529
15343
  [
14530
15344
  'import type { Plugin } from "@opencode-ai/plugin"',
@@ -14580,7 +15394,7 @@ ${content.slice(frontmatterMatch[0].length)}`;
14580
15394
  function syncOpenCodeSkills(pluginDir, pluginName) {
14581
15395
  const sourceSkillsDir = resolve15(pluginDir, "skills");
14582
15396
  if (!existsSync22(sourceSkillsDir)) return;
14583
- mkdirSync3(getOpenCodeSkillRoot(), { recursive: true });
15397
+ mkdirSync4(getOpenCodeSkillRoot(), { recursive: true });
14584
15398
  for (const entry of readdirSync8(sourceSkillsDir, { withFileTypes: true })) {
14585
15399
  if (!entry.isDirectory()) continue;
14586
15400
  const skillSourceDir = resolve15(sourceSkillsDir, entry.name);
@@ -14589,7 +15403,7 @@ function syncOpenCodeSkills(pluginDir, pluginName) {
14589
15403
  rmSync3(installedSkillDir, { recursive: true, force: true });
14590
15404
  cpSync3(skillSourceDir, installedSkillDir, { recursive: true });
14591
15405
  const skillPath = resolve15(installedSkillDir, "SKILL.md");
14592
- writeFileSync3(
15406
+ writeFileSync4(
14593
15407
  skillPath,
14594
15408
  namespaceOpenCodeSkill(readFileSync10(skillPath, "utf-8"), pluginName, entry.name)
14595
15409
  );
@@ -14641,6 +15455,15 @@ function getInstallFollowupNotes(platforms) {
14641
15455
  if (platforms.includes("claude-code")) {
14642
15456
  notes.push("Claude Code note: if Claude is already open, run /reload-plugins in the session to pick up the new install.");
14643
15457
  }
15458
+ if (platforms.includes("cursor")) {
15459
+ notes.push("Cursor note: if Cursor is already open, use Developer: Reload Window or restart Cursor to pick up the new install.");
15460
+ }
15461
+ if (platforms.includes("codex")) {
15462
+ 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.");
15463
+ }
15464
+ if (platforms.includes("opencode")) {
15465
+ notes.push("OpenCode note: if OpenCode is already open, restart or reload it so the plugin is picked up.");
15466
+ }
14644
15467
  return notes;
14645
15468
  }
14646
15469
  function runCommandDefault(command2, args2) {
@@ -14653,7 +15476,7 @@ function runCommandDefault(command2, args2) {
14653
15476
  }
14654
15477
  function createSymlinkInstall(target) {
14655
15478
  const parentDir = resolve15(target.pluginDir, "..");
14656
- mkdirSync3(parentDir, { recursive: true });
15479
+ mkdirSync4(parentDir, { recursive: true });
14657
15480
  if (existsSync22(target.pluginDir)) {
14658
15481
  rmSync3(target.pluginDir, { recursive: true, force: true });
14659
15482
  }
@@ -14691,7 +15514,7 @@ function readCodexMarketplace(filepath) {
14691
15514
  }
14692
15515
  function ensureCodexMarketplace(pluginName) {
14693
15516
  const filepath = getCodexMarketplacePath();
14694
- mkdirSync3(dirname5(filepath), { recursive: true });
15517
+ mkdirSync4(dirname5(filepath), { recursive: true });
14695
15518
  const marketplace = readCodexMarketplace(filepath);
14696
15519
  const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName);
14697
15520
  nextPlugins.push({
@@ -14706,7 +15529,7 @@ function ensureCodexMarketplace(pluginName) {
14706
15529
  },
14707
15530
  category: "Productivity"
14708
15531
  });
14709
- writeFileSync3(
15532
+ writeFileSync4(
14710
15533
  filepath,
14711
15534
  JSON.stringify({
14712
15535
  name: marketplace.name ?? "pluxx-local",
@@ -14727,7 +15550,7 @@ function removeCodexMarketplacePlugin(pluginName) {
14727
15550
  rmSync3(filepath, { force: true });
14728
15551
  return;
14729
15552
  }
14730
- writeFileSync3(
15553
+ writeFileSync4(
14731
15554
  filepath,
14732
15555
  JSON.stringify({
14733
15556
  name: marketplace.name ?? "pluxx-local",
@@ -14738,7 +15561,7 @@ function removeCodexMarketplacePlugin(pluginName) {
14738
15561
  }
14739
15562
  function createCopiedInstall(target) {
14740
15563
  const parentDir = resolve15(target.pluginDir, "..");
14741
- mkdirSync3(parentDir, { recursive: true });
15564
+ mkdirSync4(parentDir, { recursive: true });
14742
15565
  if (existsSync22(target.pluginDir)) {
14743
15566
  rmSync3(target.pluginDir, { recursive: true, force: true });
14744
15567
  }
@@ -14791,7 +15614,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
14791
15614
  }
14792
15615
  mcpServers[name] = entry;
14793
15616
  }
14794
- writeFileSync3(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
15617
+ writeFileSync4(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
14795
15618
  return;
14796
15619
  }
14797
15620
  if (platform === "codex") {
@@ -14821,7 +15644,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
14821
15644
  }
14822
15645
  mcpServers[name] = entry;
14823
15646
  }
14824
- writeFileSync3(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
15647
+ writeFileSync4(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
14825
15648
  }
14826
15649
  }
14827
15650
  function writeInstalledUserConfig(pluginDir, entries) {
@@ -14831,13 +15654,13 @@ function writeInstalledUserConfig(pluginDir, entries) {
14831
15654
  values: buildUserConfigValueMap(entries),
14832
15655
  env: buildUserConfigEnvMap(entries)
14833
15656
  };
14834
- writeFileSync3(filepath, JSON.stringify(payload, null, 2) + "\n");
15657
+ writeFileSync4(filepath, JSON.stringify(payload, null, 2) + "\n");
14835
15658
  }
14836
15659
  function disableInstalledEnvValidation(pluginDir, entries) {
14837
15660
  if (entries.length === 0) return;
14838
15661
  const filepath = resolve15(pluginDir, "scripts/check-env.sh");
14839
15662
  if (!existsSync22(filepath)) return;
14840
- writeFileSync3(
15663
+ writeFileSync4(
14841
15664
  filepath,
14842
15665
  "#!/usr/bin/env bash\nset -euo pipefail\n# pluxx install materialized required config for this local plugin install.\nexit 0\n"
14843
15666
  );
@@ -14869,15 +15692,15 @@ function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
14869
15692
  const pluginManifestPath = resolve15(sourceDir, ".claude-plugin/plugin.json");
14870
15693
  const pluginManifest = JSON.parse(readFileSync10(pluginManifestPath, "utf-8"));
14871
15694
  rmSync3(marketplaceRoot, { recursive: true, force: true });
14872
- mkdirSync3(marketplaceManifestDir, { recursive: true });
14873
- mkdirSync3(resolve15(marketplaceRoot, "plugins"), { recursive: true });
15695
+ mkdirSync4(marketplaceManifestDir, { recursive: true });
15696
+ mkdirSync4(resolve15(marketplaceRoot, "plugins"), { recursive: true });
14874
15697
  if (materialized && materialized.entries.length > 0) {
14875
15698
  cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
14876
15699
  materializeInstalledPlugin(marketplacePluginDir, "claude-code", materialized.config, materialized.entries);
14877
15700
  } else {
14878
15701
  symlinkSync(sourceDir, marketplacePluginDir);
14879
15702
  }
14880
- writeFileSync3(
15703
+ writeFileSync4(
14881
15704
  resolve15(marketplaceManifestDir, "marketplace.json"),
14882
15705
  JSON.stringify({
14883
15706
  name: marketplaceName,
@@ -15473,6 +16296,43 @@ function checkMcpMetadataQuality(checks, metadata) {
15473
16296
  path: MCP_SCAFFOLD_METADATA_PATH
15474
16297
  });
15475
16298
  }
16299
+ function checkCompilerIntent(checks, rootDir) {
16300
+ try {
16301
+ const compilerIntent = readCompilerIntent(rootDir);
16302
+ if (!compilerIntent) return;
16303
+ if ((compilerIntent.skillPolicies?.length ?? 0) === 0) {
16304
+ addCheck2(checks, {
16305
+ level: "info",
16306
+ code: "compiler-intent-empty",
16307
+ title: "Compiler intent file present",
16308
+ detail: `${PLUXX_COMPILER_INTENT_PATH} exists but does not currently carry migrated source-host policy rows.`,
16309
+ fix: "No action needed unless you expected migrated source-host policy to survive into generated outputs.",
16310
+ path: PLUXX_COMPILER_INTENT_PATH
16311
+ });
16312
+ return;
16313
+ }
16314
+ const sourceLabels = [...new Set(
16315
+ compilerIntent.skillPolicies.map((policy) => `${policy.source.platform}:${policy.source.kind}`)
16316
+ )];
16317
+ addCheck2(checks, {
16318
+ level: "info",
16319
+ code: "compiler-intent-source-host",
16320
+ title: "Imported source-host intent still influences compilation",
16321
+ 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.`,
16322
+ fix: "Review the generated permissions, agents, and host companions to confirm the migrated source-host assumptions still match the plugin you want to ship.",
16323
+ path: PLUXX_COMPILER_INTENT_PATH
16324
+ });
16325
+ } catch (error) {
16326
+ addCheck2(checks, {
16327
+ level: "warning",
16328
+ code: "compiler-intent-invalid",
16329
+ title: "Compiler intent file could not be parsed",
16330
+ detail: error instanceof Error ? error.message : String(error),
16331
+ fix: `Repair or remove ${PLUXX_COMPILER_INTENT_PATH} and rerun pluxx doctor.`,
16332
+ path: PLUXX_COMPILER_INTENT_PATH
16333
+ });
16334
+ }
16335
+ }
15476
16336
  function checkScaffoldMetadata(checks, rootDir, config) {
15477
16337
  const metadataPath = resolve16(rootDir, MCP_SCAFFOLD_METADATA_PATH);
15478
16338
  if (!existsSync23(metadataPath)) {
@@ -15805,7 +16665,7 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
15805
16665
  function isLikelyOpenCodeInstallPath(rootDir) {
15806
16666
  const parent = dirname6(rootDir);
15807
16667
  const grandparent = dirname6(parent);
15808
- return basename5(parent) === "plugins" && basename5(grandparent) === "opencode";
16668
+ return basename6(parent) === "plugins" && basename6(grandparent) === "opencode";
15809
16669
  }
15810
16670
  function checkInstalledOpenCodeHostBridge(checks, rootDir) {
15811
16671
  if (!isLikelyOpenCodeInstallPath(rootDir)) {
@@ -15819,7 +16679,7 @@ function checkInstalledOpenCodeHostBridge(checks, rootDir) {
15819
16679
  });
15820
16680
  return;
15821
16681
  }
15822
- const pluginName = basename5(rootDir);
16682
+ const pluginName = basename6(rootDir);
15823
16683
  const entryPath = `${rootDir}.ts`;
15824
16684
  const entryRelativePath = `${pluginName}.ts`;
15825
16685
  if (!existsSync23(entryPath)) {
@@ -15860,7 +16720,7 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
15860
16720
  if (!isLikelyOpenCodeInstallPath(rootDir)) {
15861
16721
  return;
15862
16722
  }
15863
- const pluginName = basename5(rootDir);
16723
+ const pluginName = basename6(rootDir);
15864
16724
  const sourceSkillsDir = resolve16(rootDir, "skills");
15865
16725
  if (!existsSync23(sourceSkillsDir)) {
15866
16726
  addCheck2(checks, {
@@ -16033,6 +16893,7 @@ async function doctorProject(rootDir = process.cwd()) {
16033
16893
  checkMcpConfig(checks, config);
16034
16894
  checkUserConfig(checks, config);
16035
16895
  checkScaffoldMetadata(checks, rootDir, config);
16896
+ checkCompilerIntent(checks, rootDir);
16036
16897
  checkHookTrust(checks, config);
16037
16898
  for (const target of config.targets) {
16038
16899
  const limits = PLATFORM_LIMITS[target];
@@ -16159,8 +17020,8 @@ async function runBuild(rootDir, targets) {
16159
17020
  }
16160
17021
 
16161
17022
  // 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";
17023
+ import { basename as basename7, relative as relative10, resolve as resolve18 } from "path";
17024
+ import { existsSync as existsSync24, readdirSync as readdirSync10, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
16164
17025
  function detectPlatform(pluginDir) {
16165
17026
  const checks = [
16166
17027
  { dir: ".claude-plugin", platform: "claude-code" },
@@ -16622,7 +17483,7 @@ function sanitizeMigratedSkillFrontmatter(outputDir) {
16622
17483
  "---",
16623
17484
  ...lines.slice(endIndex + 1)
16624
17485
  ].join("\n");
16625
- writeFileSync4(skillPath, rewritten, "utf-8");
17486
+ writeFileSync5(skillPath, rewritten, "utf-8");
16626
17487
  }
16627
17488
  }
16628
17489
  function readTomlScalarValue(content, key) {
@@ -16646,16 +17507,6 @@ function renderMigratedAgentMarkdown(fileStem, parsed) {
16646
17507
  const agentName = toKebabCase2(parsed.name ?? fileStem) || "agent";
16647
17508
  const title = parsed.name ?? titleCaseFromDirName(agentName);
16648
17509
  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
17510
  if (parsed.developerInstructions) {
16660
17511
  bodyLines.push(parsed.developerInstructions.trim());
16661
17512
  } else {
@@ -16665,6 +17516,8 @@ function renderMigratedAgentMarkdown(fileStem, parsed) {
16665
17516
  "---",
16666
17517
  `name: ${JSON.stringify(agentName)}`,
16667
17518
  ...parsed.description ? [`description: ${JSON.stringify(parsed.description)}`] : [],
17519
+ ...parsed.model ? [`model: ${JSON.stringify(parsed.model)}`] : [],
17520
+ ...parsed.effort ? [`model_reasoning_effort: ${JSON.stringify(parsed.effort)}`] : [],
16668
17521
  "---",
16669
17522
  "",
16670
17523
  `# ${title}`,
@@ -16716,7 +17569,7 @@ function hasTopLevelFrontmatterKey(frontmatterLines, key) {
16716
17569
  function normalizeMigratedOpenCodeAgentFile(agentPath) {
16717
17570
  const original = readFileSync12(agentPath, "utf-8");
16718
17571
  const parsed = splitMarkdownFrontmatter4(original);
16719
- const fileStem = toKebabCase2(basename6(agentPath, ".md")) || "agent";
17572
+ const fileStem = toKebabCase2(basename7(agentPath, ".md")) || "agent";
16720
17573
  const fallbackDescription = buildFallbackAgentDescription(fileStem);
16721
17574
  if (!parsed.hasFrontmatter) {
16722
17575
  const rewritten2 = [
@@ -16728,7 +17581,7 @@ function normalizeMigratedOpenCodeAgentFile(agentPath) {
16728
17581
  original.trimEnd(),
16729
17582
  ""
16730
17583
  ].join("\n");
16731
- writeFileSync4(agentPath, rewritten2, "utf-8");
17584
+ writeFileSync5(agentPath, rewritten2, "utf-8");
16732
17585
  return true;
16733
17586
  }
16734
17587
  const additions = [];
@@ -16749,7 +17602,7 @@ function normalizeMigratedOpenCodeAgentFile(agentPath) {
16749
17602
  "---",
16750
17603
  parsed.body
16751
17604
  ].join("\n");
16752
- writeFileSync4(agentPath, rewritten, "utf-8");
17605
+ writeFileSync5(agentPath, rewritten, "utf-8");
16753
17606
  return true;
16754
17607
  }
16755
17608
  function walkMarkdownFiles3(dir) {
@@ -16781,13 +17634,13 @@ function copyCodexAgents(sourceDir, destDir) {
16781
17634
  const entries = readdirSync10(sourceDir, { withFileTypes: true });
16782
17635
  const tomlEntries = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".toml"));
16783
17636
  if (tomlEntries.length === 0) return false;
16784
- mkdirSync4(destDir, { recursive: true });
17637
+ mkdirSync5(destDir, { recursive: true });
16785
17638
  for (const entry of tomlEntries) {
16786
17639
  const sourcePath = resolve18(sourceDir, entry.name);
16787
17640
  const parsed = parseCodexAgentToml(readFileSync12(sourcePath, "utf-8"));
16788
17641
  const fallbackName = entry.name.replace(/\.toml$/, "");
16789
17642
  const fileName = `${toKebabCase2(parsed.name ?? fallbackName) || "agent"}.md`;
16790
- writeFileSync4(resolve18(destDir, fileName), renderMigratedAgentMarkdown(fallbackName, parsed), "utf-8");
17643
+ writeFileSync5(resolve18(destDir, fileName), renderMigratedAgentMarkdown(fallbackName, parsed), "utf-8");
16791
17644
  }
16792
17645
  return true;
16793
17646
  }
@@ -17179,7 +18032,7 @@ Generated pluxx.config.ts`);
17179
18032
  }
17180
18033
  const taxonomyPath = resolve18(outputDir, MCP_TAXONOMY_PATH);
17181
18034
  const metadataPath = resolve18(outputDir, MCP_SCAFFOLD_METADATA_PATH);
17182
- mkdirSync4(resolve18(outputDir, ".pluxx"), { recursive: true });
18035
+ mkdirSync5(resolve18(outputDir, ".pluxx"), { recursive: true });
17183
18036
  await writeTextFile(taxonomyPath, `${JSON.stringify(result.persistedSkills, null, 2)}
17184
18037
  `);
17185
18038
  if (result.compilerIntent) {
@@ -17206,7 +18059,7 @@ Generated pluxx.config.ts`);
17206
18059
  }
17207
18060
 
17208
18061
  // src/cli/mcp-proxy.ts
17209
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync13 } from "fs";
18062
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync13 } from "fs";
17210
18063
  import { dirname as dirname7, resolve as resolve19 } from "path";
17211
18064
  import * as readline3 from "readline";
17212
18065
  function usage() {
@@ -17219,19 +18072,19 @@ function usage() {
17219
18072
  "- --replay serves a deterministic stdio MCP session from a recorded tape."
17220
18073
  ].join("\n");
17221
18074
  }
17222
- function readOption(rawArgs, flag) {
17223
- const index = rawArgs.indexOf(flag);
18075
+ function readOption(rawArgs2, flag) {
18076
+ const index = rawArgs2.indexOf(flag);
17224
18077
  if (index === -1) return void 0;
17225
- const value = rawArgs[index + 1];
18078
+ const value = rawArgs2[index + 1];
17226
18079
  if (!value || value.startsWith("-")) {
17227
18080
  return void 0;
17228
18081
  }
17229
18082
  return value;
17230
18083
  }
17231
- function parseOptions(rawArgs) {
17232
- const source = readOption(rawArgs, "--from-mcp");
17233
- const recordPath = readOption(rawArgs, "--record");
17234
- const replayPath = readOption(rawArgs, "--replay");
18084
+ function parseOptions(rawArgs2) {
18085
+ const source = readOption(rawArgs2, "--from-mcp");
18086
+ const recordPath = readOption(rawArgs2, "--record");
18087
+ const replayPath = readOption(rawArgs2, "--replay");
17235
18088
  if (recordPath && replayPath) {
17236
18089
  throw new Error("Choose either --record or --replay, not both.");
17237
18090
  }
@@ -17284,7 +18137,7 @@ async function loadReplayTape(filepath) {
17284
18137
  }
17285
18138
  async function writeTape(filepath, tape) {
17286
18139
  const absolutePath = resolve19(process.cwd(), filepath);
17287
- mkdirSync5(dirname7(absolutePath), { recursive: true });
18140
+ mkdirSync6(dirname7(absolutePath), { recursive: true });
17288
18141
  await writeTextFile(absolutePath, `${JSON.stringify(tape, null, 2)}
17289
18142
  `);
17290
18143
  }
@@ -17419,17 +18272,17 @@ async function replaySession(filepath, io) {
17419
18272
  rl.close();
17420
18273
  }
17421
18274
  }
17422
- async function runMcpProxy(rawArgs) {
17423
- return await runMcpProxyWithIo(rawArgs, {
18275
+ async function runMcpProxy(rawArgs2) {
18276
+ return await runMcpProxyWithIo(rawArgs2, {
17424
18277
  input: process.stdin,
17425
18278
  output: process.stdout,
17426
18279
  error: process.stderr
17427
18280
  });
17428
18281
  }
17429
- async function runMcpProxyWithIo(rawArgs, io) {
18282
+ async function runMcpProxyWithIo(rawArgs2, io) {
17430
18283
  let options;
17431
18284
  try {
17432
- options = parseOptions(rawArgs);
18285
+ options = parseOptions(rawArgs2);
17433
18286
  } catch (error) {
17434
18287
  io.error.write(`${error instanceof Error ? error.message : String(error)}
17435
18288
 
@@ -17455,7 +18308,7 @@ var PromptCancelledError = class extends Error {
17455
18308
  }
17456
18309
  };
17457
18310
  function ask(question) {
17458
- return new Promise((resolve23, reject) => {
18311
+ return new Promise((resolve24, reject) => {
17459
18312
  const rl = readline4.createInterface({
17460
18313
  input: process.stdin,
17461
18314
  output: process.stdout
@@ -17483,7 +18336,7 @@ function ask(question) {
17483
18336
  rl.once("close", onClose);
17484
18337
  rl.question(question, (answer) => {
17485
18338
  settle(() => {
17486
- resolve23(answer);
18339
+ resolve24(answer);
17487
18340
  rl.close();
17488
18341
  });
17489
18342
  });
@@ -18458,13 +19311,13 @@ ${c2}
18458
19311
  } }).prompt();
18459
19312
 
18460
19313
  // 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";
19314
+ import { basename as basename8, resolve as resolve23 } from "path";
19315
+ import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
19316
+ import { tmpdir as tmpdir5 } from "os";
19317
+ import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
18465
19318
 
18466
19319
  // src/cli/publish.ts
18467
- import { chmodSync, existsSync as existsSync25, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
19320
+ import { chmodSync, existsSync as existsSync25, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
18468
19321
  import { createHash } from "crypto";
18469
19322
  import { resolve as resolve20 } from "path";
18470
19323
  import { spawnSync as spawnSync2 } from "child_process";
@@ -18604,9 +19457,9 @@ function collectChecks(args2) {
18604
19457
  const gitStatus = args2.runCommand("git", ["status", "--porcelain"], { cwd: args2.rootDir });
18605
19458
  checks.push({
18606
19459
  name: "git-clean",
18607
- ok: gitStatus.status === 0 && gitStatus.stdout.trim() === "",
19460
+ ok: args2.allowDirty || gitStatus.status === 0 && gitStatus.stdout.trim() === "",
18608
19461
  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."
19462
+ 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
19463
  });
18611
19464
  if (args2.npmEnabled) {
18612
19465
  checks.push({
@@ -18658,6 +19511,7 @@ function planPublish(config, options = {}) {
18658
19511
  config,
18659
19512
  npmEnabled,
18660
19513
  githubReleaseEnabled,
19514
+ allowDirty: options.allowDirty ?? false,
18661
19515
  packageDir,
18662
19516
  packageName,
18663
19517
  githubRepo,
@@ -18921,7 +19775,7 @@ rm -rf "$INSTALL_DIR"
18921
19775
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
18922
19776
 
18923
19777
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
18924
- echo "If Cursor is already open, restart or reload it so the plugin is picked up."
19778
+ echo "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up."
18925
19779
  `;
18926
19780
  }
18927
19781
  function renderInstallCodexScript(config) {
@@ -19035,7 +19889,7 @@ NODE
19035
19889
 
19036
19890
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
19037
19891
  echo "Updated Codex marketplace catalog at $MARKETPLACE_PATH"
19038
- echo "If Codex is already open, restart it so the plugin is picked up."
19892
+ 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
19893
  `;
19040
19894
  }
19041
19895
  function renderInstallOpenCodeScript(config) {
@@ -19210,7 +20064,7 @@ function writeChecksumFile(tempRoot, files) {
19210
20064
  const name = filePath.split("/").pop();
19211
20065
  return `${digest} ${name}`;
19212
20066
  }).join("\n");
19213
- writeFileSync5(checksumPath, `${lines}
20067
+ writeFileSync6(checksumPath, `${lines}
19214
20068
  `);
19215
20069
  return checksumPath;
19216
20070
  }
@@ -19249,14 +20103,14 @@ function createReleaseArtifacts(rootDir, config, plan, runCommand) {
19249
20103
  for (const asset of githubRelease.assets) {
19250
20104
  if (asset.kind !== "installer") continue;
19251
20105
  const installerPath = resolve20(tempRoot, asset.name);
19252
- writeFileSync5(installerPath, renderInstallerScript(asset, config, context));
20106
+ writeFileSync6(installerPath, renderInstallerScript(asset, config, context));
19253
20107
  chmodSync(installerPath, 493);
19254
20108
  created.push(installerPath);
19255
20109
  }
19256
20110
  const manifestAsset = githubRelease.assets.find((asset) => asset.kind === "manifest");
19257
20111
  if (manifestAsset) {
19258
20112
  const manifestPath = resolve20(tempRoot, manifestAsset.name);
19259
- writeFileSync5(manifestPath, buildReleaseManifest(config, context));
20113
+ writeFileSync6(manifestPath, buildReleaseManifest(config, context));
19260
20114
  created.push(manifestPath);
19261
20115
  }
19262
20116
  const checksumAsset = githubRelease.assets.find((asset) => asset.kind === "checksum");
@@ -19371,36 +20225,36 @@ function runPublish(config, options = {}) {
19371
20225
  }
19372
20226
 
19373
20227
  // src/cli/runtime.ts
19374
- function createCliRuntime(rawArgs) {
20228
+ function createCliRuntime(rawArgs2) {
19375
20229
  const isCI = process.env.CI === "1" || process.env.CI === "true";
19376
20230
  const isTTY = process.stdin.isTTY === true && process.stdout.isTTY === true;
19377
20231
  return {
19378
- dryRun: rawArgs.includes("--dry-run"),
19379
- jsonOutput: rawArgs.includes("--json"),
19380
- quiet: rawArgs.includes("--quiet"),
20232
+ dryRun: rawArgs2.includes("--dry-run"),
20233
+ jsonOutput: rawArgs2.includes("--json"),
20234
+ quiet: rawArgs2.includes("--quiet"),
19381
20235
  isCI,
19382
20236
  isTTY,
19383
20237
  isInteractive: isTTY && !isCI
19384
20238
  };
19385
20239
  }
19386
- function readFlag(rawArgs, flag) {
19387
- return rawArgs.includes(flag);
20240
+ function readFlag(rawArgs2, flag) {
20241
+ return rawArgs2.includes(flag);
19388
20242
  }
19389
- function readOption2(rawArgs, flag) {
19390
- const index = rawArgs.indexOf(flag);
20243
+ function readOption2(rawArgs2, flag) {
20244
+ const index = rawArgs2.indexOf(flag);
19391
20245
  if (index === -1) return void 0;
19392
- const value = rawArgs[index + 1];
20246
+ const value = rawArgs2[index + 1];
19393
20247
  if (!value || value.startsWith("-")) {
19394
20248
  return void 0;
19395
20249
  }
19396
20250
  return value;
19397
20251
  }
19398
- function readMultiValueOption(rawArgs, flag) {
19399
- const index = rawArgs.indexOf(flag);
20252
+ function readMultiValueOption(rawArgs2, flag) {
20253
+ const index = rawArgs2.indexOf(flag);
19400
20254
  if (index === -1) return void 0;
19401
20255
  const values = [];
19402
- for (let i = index + 1; i < rawArgs.length; i += 1) {
19403
- const value = rawArgs[i];
20256
+ for (let i = index + 1; i < rawArgs2.length; i += 1) {
20257
+ const value = rawArgs2[i];
19404
20258
  if (value.startsWith("-")) break;
19405
20259
  values.push(value);
19406
20260
  }
@@ -19464,8 +20318,225 @@ function printVerifyInstallResult(result) {
19464
20318
  console.log(result.ok ? "pluxx verify-install passed." : "pluxx verify-install failed.");
19465
20319
  }
19466
20320
 
20321
+ // src/cli/behavioral.ts
20322
+ import { existsSync as existsSync27, readFileSync as readFileSync15 } from "fs";
20323
+ import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
20324
+ import { tmpdir as tmpdir4 } from "os";
20325
+ import { resolve as resolve22 } from "path";
20326
+ import { spawn as spawn3 } from "child_process";
20327
+ var BEHAVIORAL_CONFIG_PATH = ".pluxx/behavioral-smoke.json";
20328
+ var CURSOR_RUNNER_BINARIES2 = ["agent", "cursor-agent"];
20329
+ var SUPPORTED_PLATFORMS = ["claude-code", "cursor", "codex", "opencode"];
20330
+ async function runBehavioralSuite(rootDir, config, targets, options = {}) {
20331
+ const selectedPlatforms = targets.filter(
20332
+ (target) => SUPPORTED_PLATFORMS.includes(target)
20333
+ );
20334
+ const cases = loadBehavioralCases(rootDir, selectedPlatforms, options.promptOverride);
20335
+ const checks = [];
20336
+ for (const behavioralCase of cases) {
20337
+ for (const platform of selectedPlatforms) {
20338
+ const targetConfig = behavioralCase.targets[platform];
20339
+ if (!targetConfig) continue;
20340
+ checks.push(await runBehavioralCheck(rootDir, config, behavioralCase.name, platform, targetConfig));
20341
+ }
20342
+ }
20343
+ return {
20344
+ ok: checks.every((check) => check.ok),
20345
+ source: options.promptOverride ? "--behavioral-prompt" : BEHAVIORAL_CONFIG_PATH,
20346
+ checks
20347
+ };
20348
+ }
20349
+ function loadBehavioralCases(rootDir, targets, promptOverride) {
20350
+ if (promptOverride) {
20351
+ return [{
20352
+ name: "inline-prompt",
20353
+ targets: Object.fromEntries(
20354
+ targets.map((target) => [target, { prompt: promptOverride }])
20355
+ )
20356
+ }];
20357
+ }
20358
+ const filePath = resolve22(rootDir, BEHAVIORAL_CONFIG_PATH);
20359
+ if (!existsSync27(filePath)) {
20360
+ throw new Error(
20361
+ `No behavioral smoke config found at ${BEHAVIORAL_CONFIG_PATH}. Add that file or pass --behavioral-prompt to define a real example query.`
20362
+ );
20363
+ }
20364
+ const parsed = JSON.parse(readFileSync15(filePath, "utf-8"));
20365
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.cases) || parsed.cases.length === 0) {
20366
+ throw new Error(`${BEHAVIORAL_CONFIG_PATH} must contain a non-empty "cases" array.`);
20367
+ }
20368
+ return parsed.cases;
20369
+ }
20370
+ async function runBehavioralCheck(rootDir, config, caseName, platform, targetConfig) {
20371
+ const prompt = targetConfig.prompt.trim();
20372
+ if (!prompt) {
20373
+ throw new Error(`Behavioral smoke case "${caseName}" for ${platform} is missing a prompt.`);
20374
+ }
20375
+ const command2 = await buildBehavioralCommand(platform, prompt, rootDir);
20376
+ const execution = await executeBehavioralCommand(platform, command2, rootDir);
20377
+ const responseText = execution.response.trim();
20378
+ const failures = [];
20379
+ if (execution.exitCode !== 0) {
20380
+ failures.push(`runner exited with code ${execution.exitCode}`);
20381
+ }
20382
+ if (!responseText) {
20383
+ failures.push("runner returned no response text");
20384
+ }
20385
+ for (const required of targetConfig.require ?? []) {
20386
+ if (!includesNeedle(responseText, required)) {
20387
+ failures.push(`missing required text: ${required}`);
20388
+ }
20389
+ }
20390
+ for (const forbidden of targetConfig.forbid ?? []) {
20391
+ if (includesNeedle(responseText, forbidden)) {
20392
+ failures.push(`matched forbidden text: ${forbidden}`);
20393
+ }
20394
+ }
20395
+ return {
20396
+ caseName,
20397
+ platform,
20398
+ prompt,
20399
+ command: command2,
20400
+ ok: failures.length === 0,
20401
+ exitCode: execution.exitCode,
20402
+ responseBytes: responseText.length,
20403
+ responsePreview: truncate2(responseText, 220),
20404
+ require: targetConfig.require,
20405
+ forbid: targetConfig.forbid,
20406
+ failures
20407
+ };
20408
+ }
20409
+ async function buildBehavioralCommand(platform, prompt, workspace) {
20410
+ if (platform === "claude-code") {
20411
+ return [
20412
+ "claude",
20413
+ "--no-session-persistence",
20414
+ "--output-format",
20415
+ "text",
20416
+ "--permission-mode",
20417
+ "acceptEdits",
20418
+ "-p",
20419
+ prompt
20420
+ ];
20421
+ }
20422
+ if (platform === "cursor") {
20423
+ const binary = await resolveCursorBinary2();
20424
+ if (!binary) {
20425
+ throw new Error("Cursor CLI `agent` or `cursor-agent` is not available on PATH.");
20426
+ }
20427
+ await ensureCursorAuthenticated(binary);
20428
+ return [
20429
+ binary,
20430
+ "-p",
20431
+ "--trust",
20432
+ "--workspace",
20433
+ workspace,
20434
+ "--force",
20435
+ prompt
20436
+ ];
20437
+ }
20438
+ if (platform === "codex") {
20439
+ return [
20440
+ "codex",
20441
+ "exec",
20442
+ "--ephemeral",
20443
+ "--skip-git-repo-check",
20444
+ "--full-auto",
20445
+ prompt
20446
+ ];
20447
+ }
20448
+ return ["opencode", "run", prompt];
20449
+ }
20450
+ async function executeBehavioralCommand(platform, command2, cwd) {
20451
+ let codexOutputDir = null;
20452
+ let codexLastMessagePath = null;
20453
+ const runtimeCommand = [...command2];
20454
+ if (platform === "codex") {
20455
+ codexOutputDir = await mkdtemp2(resolve22(tmpdir4(), "pluxx-codex-behavioral-"));
20456
+ codexLastMessagePath = resolve22(codexOutputDir, "last-message.txt");
20457
+ runtimeCommand.splice(2, 0, "--output-last-message", codexLastMessagePath);
20458
+ }
20459
+ try {
20460
+ return await new Promise((resolvePromise, reject) => {
20461
+ const child = spawn3(runtimeCommand[0], runtimeCommand.slice(1), {
20462
+ cwd,
20463
+ stdio: ["ignore", "pipe", "pipe"],
20464
+ env: process.env
20465
+ });
20466
+ const stdoutChunks = [];
20467
+ const stderrChunks = [];
20468
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
20469
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
20470
+ child.on("error", reject);
20471
+ child.on("close", (code) => {
20472
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
20473
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
20474
+ const codexMessage = codexLastMessagePath && existsSync27(codexLastMessagePath) ? readFileSync15(codexLastMessagePath, "utf-8") : "";
20475
+ resolvePromise({
20476
+ exitCode: code ?? 1,
20477
+ response: codexMessage.trim() || stdout.trim() || stderr.trim()
20478
+ });
20479
+ });
20480
+ });
20481
+ } finally {
20482
+ if (codexOutputDir) {
20483
+ await rm3(codexOutputDir, { recursive: true, force: true });
20484
+ }
20485
+ }
20486
+ }
20487
+ async function resolveCursorBinary2() {
20488
+ for (const candidate of CURSOR_RUNNER_BINARIES2) {
20489
+ if (await commandExists2(candidate)) {
20490
+ return candidate;
20491
+ }
20492
+ }
20493
+ return void 0;
20494
+ }
20495
+ async function ensureCursorAuthenticated(binary) {
20496
+ if (process.env.CURSOR_API_KEY && process.env.CURSOR_API_KEY.trim()) {
20497
+ return;
20498
+ }
20499
+ const ok = await commandSucceeds2([binary, "status"]);
20500
+ if (!ok) {
20501
+ throw new Error("Cursor CLI authentication is required. Run `agent login` (or `cursor-agent login`) or export `CURSOR_API_KEY` before behavioral smoke runs.");
20502
+ }
20503
+ }
20504
+ async function commandExists2(binary) {
20505
+ return await new Promise((resolvePromise) => {
20506
+ const child = spawn3("sh", ["-c", `command -v ${shellQuote2(binary)} >/dev/null 2>&1`], {
20507
+ stdio: "ignore",
20508
+ env: process.env
20509
+ });
20510
+ child.on("close", (code) => resolvePromise(code === 0));
20511
+ child.on("error", () => resolvePromise(false));
20512
+ });
20513
+ }
20514
+ async function commandSucceeds2(command2) {
20515
+ return await new Promise((resolvePromise) => {
20516
+ const child = spawn3(command2[0], command2.slice(1), {
20517
+ stdio: "ignore",
20518
+ env: process.env
20519
+ });
20520
+ child.on("close", (code) => resolvePromise(code === 0));
20521
+ child.on("error", () => resolvePromise(false));
20522
+ });
20523
+ }
20524
+ function truncate2(value, length) {
20525
+ if (value.length <= length) return value;
20526
+ return `${value.slice(0, Math.max(0, length - 3))}...`;
20527
+ }
20528
+ function includesNeedle(haystack, needle) {
20529
+ return haystack.toLowerCase().includes(needle.trim().toLowerCase());
20530
+ }
20531
+ function shellQuote2(value) {
20532
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
20533
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20534
+ }
20535
+
19467
20536
  // src/cli/index.ts
19468
- var args = process.argv.slice(2);
20537
+ var CLI_PACKAGE_NAME = "@orchid-labs/pluxx";
20538
+ var rawArgs = process.argv.slice(2);
20539
+ var args = normalizeTopLevelArgs(rawArgs);
19469
20540
  var command = args[0];
19470
20541
  var runtime = createCliRuntime(args);
19471
20542
  var DEFAULT_INIT_TARGETS = ["claude-code", "cursor", "codex", "opencode"];
@@ -19485,6 +20556,12 @@ var ALL_TARGET_PLATFORMS = [
19485
20556
  ];
19486
20557
  async function main() {
19487
20558
  switch (command) {
20559
+ case "version":
20560
+ await runVersionCommand();
20561
+ break;
20562
+ case "upgrade":
20563
+ await runUpgradeCommand();
20564
+ break;
19488
20565
  case "build":
19489
20566
  await runBuild2();
19490
20567
  break;
@@ -19548,6 +20625,95 @@ async function main() {
19548
20625
  process.exit(1);
19549
20626
  }
19550
20627
  }
20628
+ function normalizeTopLevelArgs(input) {
20629
+ if (input[0] === "--version" || input[0] === "-v") {
20630
+ return ["version", ...input.slice(1)];
20631
+ }
20632
+ if (input[0] === "--upgrade") {
20633
+ return ["upgrade", ...input.slice(1)];
20634
+ }
20635
+ return input;
20636
+ }
20637
+ function getCliPackageVersion() {
20638
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
20639
+ const raw = JSON.parse(readFileSync16(packageJsonPath, "utf-8"));
20640
+ if (typeof raw.version !== "string" || raw.version.trim() === "") {
20641
+ throw new Error("Unable to determine the installed pluxx version from package.json.");
20642
+ }
20643
+ return raw.version.trim();
20644
+ }
20645
+ function resolveNpmExecutable() {
20646
+ return process.platform === "win32" ? "npm.cmd" : "npm";
20647
+ }
20648
+ function buildUpgradeSummary() {
20649
+ const requestedVersion = readOption2(args, "--version") ?? "latest";
20650
+ const specifier = `${CLI_PACKAGE_NAME}@${requestedVersion}`;
20651
+ return {
20652
+ dryRun: runtime.dryRun,
20653
+ packageName: CLI_PACKAGE_NAME,
20654
+ currentVersion: getCliPackageVersion(),
20655
+ requestedVersion,
20656
+ specifier,
20657
+ command: [resolveNpmExecutable(), "install", "-g", specifier],
20658
+ note: "This updates the global npm install used by `pluxx` on your PATH. Repo-local and `npx` invocations are separate entrypoints."
20659
+ };
20660
+ }
20661
+ async function runVersionCommand() {
20662
+ const version = getCliPackageVersion();
20663
+ if (runtime.jsonOutput) {
20664
+ printJson({ version });
20665
+ return;
20666
+ }
20667
+ console.log(version);
20668
+ }
20669
+ async function runUpgradeCommand() {
20670
+ const summary = buildUpgradeSummary();
20671
+ if (runtime.dryRun) {
20672
+ if (runtime.jsonOutput) {
20673
+ printJson(summary);
20674
+ return;
20675
+ }
20676
+ if (!runtime.quiet) {
20677
+ console.log(`Dry run: would run \`${summary.command.join(" ")}\``);
20678
+ console.log(summary.note);
20679
+ console.log(`Current version: ${summary.currentVersion}`);
20680
+ }
20681
+ return;
20682
+ }
20683
+ const install = spawnSync3(summary.command[0], summary.command.slice(1), runtime.jsonOutput ? {
20684
+ env: process.env,
20685
+ encoding: "utf-8",
20686
+ stdio: "pipe"
20687
+ } : {
20688
+ env: process.env,
20689
+ stdio: "inherit"
20690
+ });
20691
+ if (install.status !== 0) {
20692
+ if (runtime.jsonOutput) {
20693
+ printJson({
20694
+ ...summary,
20695
+ ok: false,
20696
+ stdout: typeof install.stdout === "string" ? install.stdout : "",
20697
+ stderr: typeof install.stderr === "string" ? install.stderr : "",
20698
+ exitCode: install.status ?? 1
20699
+ });
20700
+ }
20701
+ throw new Error(`Failed to upgrade ${CLI_PACKAGE_NAME}.`);
20702
+ }
20703
+ const result = {
20704
+ ...summary,
20705
+ ok: true
20706
+ };
20707
+ if (runtime.jsonOutput) {
20708
+ printJson(result);
20709
+ return;
20710
+ }
20711
+ if (!runtime.quiet) {
20712
+ console.log(`Upgraded ${summary.packageName} with \`${summary.command.join(" ")}\`.`);
20713
+ console.log("Run `pluxx --version` to verify the active version on your PATH.");
20714
+ console.log(summary.note);
20715
+ }
20716
+ }
19551
20717
  function hasAgentContextHints(input) {
19552
20718
  return Boolean(input.docsUrl || input.websiteUrl || (input.contextPaths?.length ?? 0) > 0);
19553
20719
  }
@@ -19833,8 +20999,8 @@ function parseTargetPlatforms(raw) {
19833
20999
  }
19834
21000
  return targets;
19835
21001
  }
19836
- function parseTargetFlagValues(rawArgs) {
19837
- const values = readMultiValueOption(rawArgs, "--target");
21002
+ function parseTargetFlagValues(rawArgs2) {
21003
+ const values = readMultiValueOption(rawArgs2, "--target");
19838
21004
  if (!values) return void 0;
19839
21005
  return parseTargetPlatforms(values.join(","));
19840
21006
  }
@@ -19912,7 +21078,7 @@ function defaultAuthEnvVar(provider, discoveredAuth) {
19912
21078
  function tryOpenBrowser(url) {
19913
21079
  const launcher = process.platform === "darwin" ? { command: "open", args: [url] } : process.platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
19914
21080
  try {
19915
- const child = spawn3(launcher.command, launcher.args, {
21081
+ const child = spawn4(launcher.command, launcher.args, {
19916
21082
  detached: true,
19917
21083
  stdio: "ignore"
19918
21084
  });
@@ -20130,7 +21296,7 @@ async function planInitContextArtifactFiles(rootDir, contextPack) {
20130
21296
  return plannedFiles;
20131
21297
  }
20132
21298
  async function planAuxiliaryFile(rootDir, relativePath, content) {
20133
- const filePath = resolve22(rootDir, relativePath);
21299
+ const filePath = resolve23(rootDir, relativePath);
20134
21300
  const action = await planTextFileAction(filePath, content);
20135
21301
  return {
20136
21302
  relativePath,
@@ -20157,29 +21323,29 @@ function formatMcpDiscoverySummary(introspection) {
20157
21323
  }
20158
21324
  return `${parts.join(", ")} discovered`;
20159
21325
  }
20160
- function parseInitFromMcpOptions(rawArgs, initialName, initialSource) {
21326
+ function parseInitFromMcpOptions(rawArgs2, initialName, initialSource) {
20161
21327
  return {
20162
- source: initialSource ?? readOption2(rawArgs, "--from-mcp"),
20163
- assumeDefaults: rawArgs.includes("--yes"),
20164
- name: readOption2(rawArgs, "--name") ?? initialName,
20165
- author: readOption2(rawArgs, "--author"),
20166
- displayName: readOption2(rawArgs, "--display-name"),
20167
- targets: readOption2(rawArgs, "--targets"),
20168
- docsUrl: readOption2(rawArgs, "--docs"),
20169
- websiteUrl: readOption2(rawArgs, "--website"),
20170
- contextPaths: readMultiValueOption(rawArgs, "--context"),
20171
- ingestProvider: readOption2(rawArgs, "--ingest-provider"),
20172
- authEnv: readOption2(rawArgs, "--auth-env"),
20173
- authType: readOption2(rawArgs, "--auth-type"),
20174
- authHeader: readOption2(rawArgs, "--auth-header"),
20175
- authTemplate: readOption2(rawArgs, "--auth-template"),
20176
- runtimeAuth: readOption2(rawArgs, "--runtime-auth"),
20177
- oauthWrapper: rawArgs.includes("--oauth-wrapper"),
20178
- approveMcpTools: rawArgs.includes("--approve-mcp-tools"),
20179
- grouping: readOption2(rawArgs, "--grouping"),
20180
- hooks: readOption2(rawArgs, "--hooks"),
20181
- transport: readOption2(rawArgs, "--transport"),
20182
- jsonOutput: rawArgs.includes("--json")
21328
+ source: initialSource ?? readOption2(rawArgs2, "--from-mcp"),
21329
+ assumeDefaults: rawArgs2.includes("--yes"),
21330
+ name: readOption2(rawArgs2, "--name") ?? initialName,
21331
+ author: readOption2(rawArgs2, "--author"),
21332
+ displayName: readOption2(rawArgs2, "--display-name"),
21333
+ targets: readOption2(rawArgs2, "--targets"),
21334
+ docsUrl: readOption2(rawArgs2, "--docs"),
21335
+ websiteUrl: readOption2(rawArgs2, "--website"),
21336
+ contextPaths: readMultiValueOption(rawArgs2, "--context"),
21337
+ ingestProvider: readOption2(rawArgs2, "--ingest-provider"),
21338
+ authEnv: readOption2(rawArgs2, "--auth-env"),
21339
+ authType: readOption2(rawArgs2, "--auth-type"),
21340
+ authHeader: readOption2(rawArgs2, "--auth-header"),
21341
+ authTemplate: readOption2(rawArgs2, "--auth-template"),
21342
+ runtimeAuth: readOption2(rawArgs2, "--runtime-auth"),
21343
+ oauthWrapper: rawArgs2.includes("--oauth-wrapper"),
21344
+ approveMcpTools: rawArgs2.includes("--approve-mcp-tools"),
21345
+ grouping: readOption2(rawArgs2, "--grouping"),
21346
+ hooks: readOption2(rawArgs2, "--hooks"),
21347
+ transport: readOption2(rawArgs2, "--transport"),
21348
+ jsonOutput: rawArgs2.includes("--json")
20183
21349
  };
20184
21350
  }
20185
21351
  function toKebabCase3(value) {
@@ -20199,7 +21365,7 @@ async function runInit() {
20199
21365
  if (!runtime.isInteractive) {
20200
21366
  throw new Error("pluxx init requires an interactive terminal unless you use `pluxx init --from-mcp ... --yes`.");
20201
21367
  }
20202
- const dirName = positionalName ? toKebabCase3(positionalName) : basename7(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
21368
+ const dirName = positionalName ? toKebabCase3(positionalName) : basename8(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
20203
21369
  console.log("");
20204
21370
  console.log(" pluxx init \u2014 Create a new plugin");
20205
21371
  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 +21442,7 @@ ${mcpBlock}${brandBlock}
20276
21442
  targets: [${targetsList}],
20277
21443
  })
20278
21444
  `;
20279
- await writeTextFile(resolve22(process.cwd(), "pluxx.config.ts"), template);
21445
+ await writeTextFile(resolve23(process.cwd(), "pluxx.config.ts"), template);
20280
21446
  const skillDir = `skills/${skillName}`;
20281
21447
  await mkdir4(skillDir, { recursive: true });
20282
21448
  const skillContent = `---
@@ -20298,7 +21464,7 @@ Describe how agents should use this skill.
20298
21464
  Example prompt or command here
20299
21465
  \`\`\`
20300
21466
  `;
20301
- await writeTextFile(resolve22(process.cwd(), `${skillDir}/SKILL.md`), skillContent);
21467
+ await writeTextFile(resolve23(process.cwd(), `${skillDir}/SKILL.md`), skillContent);
20302
21468
  console.log("");
20303
21469
  console.log(" Created:");
20304
21470
  console.log(" pluxx.config.ts");
@@ -20542,7 +21708,7 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
20542
21708
  if (!runtime.dryRun) {
20543
21709
  await applyMcpScaffoldPlan(process.cwd(), plan);
20544
21710
  for (const file of contextArtifactFiles) {
20545
- await writeTextFile(resolve22(process.cwd(), file.relativePath), file.content);
21711
+ await writeTextFile(resolve23(process.cwd(), file.relativePath), file.content);
20546
21712
  }
20547
21713
  }
20548
21714
  const lintResult = runtime.dryRun ? { errors: 0, warnings: 0, issues: [] } : await lintProject(process.cwd());
@@ -20705,7 +21871,7 @@ async function runSync() {
20705
21871
  async function runDoctor() {
20706
21872
  const consumerMode = readFlag(args, "--consumer");
20707
21873
  const doctorPath = args.slice(1).find((value) => !value.startsWith("-"));
20708
- const rootDir = doctorPath ? resolve22(process.cwd(), doctorPath) : process.cwd();
21874
+ const rootDir = doctorPath ? resolve23(process.cwd(), doctorPath) : process.cwd();
20709
21875
  const report = consumerMode ? await doctorConsumer(rootDir) : await doctorProject(rootDir);
20710
21876
  if (runtime.jsonOutput) {
20711
21877
  printJson(report);
@@ -21107,7 +22273,7 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
21107
22273
  review: passDecisions.review,
21108
22274
  verify
21109
22275
  });
21110
- const workspaceRoot = runtime.dryRun ? await mkdtemp2(`${tmpdir4()}/pluxx-autopilot-`) : process.cwd();
22276
+ const workspaceRoot = runtime.dryRun ? await mkdtemp3(`${tmpdir5()}/pluxx-autopilot-`) : process.cwd();
21111
22277
  tempDir = runtime.dryRun ? workspaceRoot : void 0;
21112
22278
  const scaffoldSpinner = createSpinner(runtime);
21113
22279
  scaffoldSpinner?.start(`Autopilot 2/${totalSteps} \xB7 Planning scaffold...`);
@@ -21454,11 +22620,13 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
21454
22620
  }
21455
22621
  } finally {
21456
22622
  if (tempDir) {
21457
- await rm3(tempDir, { recursive: true, force: true });
22623
+ await rm4(tempDir, { recursive: true, force: true });
21458
22624
  }
21459
22625
  }
21460
22626
  }
21461
22627
  async function runTestCommand() {
22628
+ const behavioral = readFlag(args, "--behavioral");
22629
+ const behavioralPrompt = readOption2(args, "--behavioral-prompt");
21462
22630
  const targets = parseTargetFlagValues(args);
21463
22631
  const result = await runTestSuite({
21464
22632
  rootDir: process.cwd(),
@@ -21466,15 +22634,20 @@ async function runTestCommand() {
21466
22634
  });
21467
22635
  const config = result.config.ok ? await loadConfig() : null;
21468
22636
  const platforms = targets ?? config?.targets ?? [];
22637
+ if (behavioral && !args.includes("--install")) {
22638
+ throw new Error("--behavioral requires --install so the selected host CLIs can see the installed plugin bundle.");
22639
+ }
21469
22640
  const install = result.ok && config ? await maybeInstallBuiltOutputs(config, platforms, { verifyConsumers: true }) : void 0;
21470
- const finalResult = install?.verification ? {
22641
+ const behavioralResult = result.ok && config && install && behavioral ? await runBehavioralSuite(process.cwd(), config, platforms, { promptOverride: behavioralPrompt }) : void 0;
22642
+ const finalResult = install?.verification || behavioralResult ? {
21471
22643
  ...result,
21472
- ok: result.ok && install.verification.ok
22644
+ ok: result.ok && (install?.verification?.ok ?? true) && (behavioralResult?.ok ?? true)
21473
22645
  } : result;
21474
22646
  if (runtime.jsonOutput) {
21475
22647
  printJson({
21476
22648
  ...finalResult,
21477
- install
22649
+ install,
22650
+ behavioral: behavioralResult
21478
22651
  });
21479
22652
  return;
21480
22653
  }
@@ -21492,6 +22665,18 @@ async function runTestCommand() {
21492
22665
  console.log(`${prefix} ${check.platform}: ${check.consumerPath}`);
21493
22666
  }
21494
22667
  }
22668
+ if (behavioralResult) {
22669
+ console.log("Behavioral headless smoke:");
22670
+ for (const check of behavioralResult.checks) {
22671
+ const prefix = check.ok ? " PASS" : " FAIL";
22672
+ console.log(`${prefix} ${check.platform}/${check.caseName}: ${check.responsePreview || "(no response preview)"}`);
22673
+ if (!check.ok) {
22674
+ for (const failure of check.failures) {
22675
+ console.log(` - ${failure}`);
22676
+ }
22677
+ }
22678
+ }
22679
+ }
21495
22680
  for (const note of install.notes) {
21496
22681
  console.log(note);
21497
22682
  }
@@ -21598,7 +22783,8 @@ async function runPublishCommand() {
21598
22783
  requestedChannels,
21599
22784
  version: readOption2(args, "--version"),
21600
22785
  tag: readOption2(args, "--tag"),
21601
- dryRun: runtime.dryRun
22786
+ dryRun: runtime.dryRun,
22787
+ allowDirty: args.includes("--allow-dirty")
21602
22788
  });
21603
22789
  if (runtime.dryRun) {
21604
22790
  if (runtime.jsonOutput) {
@@ -21618,7 +22804,8 @@ async function runPublishCommand() {
21618
22804
  requestedChannels,
21619
22805
  version: readOption2(args, "--version"),
21620
22806
  tag: readOption2(args, "--tag"),
21621
- dryRun: false
22807
+ dryRun: false,
22808
+ allowDirty: args.includes("--allow-dirty")
21622
22809
  });
21623
22810
  if (runtime.jsonOutput) {
21624
22811
  printJson(result);
@@ -21641,7 +22828,7 @@ async function runVerifyInstall() {
21641
22828
  const targets = parseTargetFlagValues(args);
21642
22829
  const config = await loadConfig();
21643
22830
  if (runtime.dryRun) {
21644
- const distDir = resolve22(process.cwd(), config.outDir);
22831
+ const distDir = resolve23(process.cwd(), config.outDir);
21645
22832
  const plan = planInstallPlugin(distDir, config.name, targets ?? config.targets);
21646
22833
  const summary = {
21647
22834
  dryRun: true,
@@ -21718,6 +22905,8 @@ function printHelp() {
21718
22905
  pluxx \u2014 Cross-platform AI agent plugin SDK
21719
22906
 
21720
22907
  Usage:
22908
+ pluxx --version | -v Print the installed Pluxx CLI version
22909
+ pluxx upgrade [--version x.y.z] Upgrade the global npm install of Pluxx
21721
22910
  pluxx build [--target <platforms...>] [--install] Generate platform-specific plugin files
21722
22911
  pluxx dev [--target <platforms...>] Watch for changes and auto-rebuild
21723
22912
  pluxx validate Validate your config
@@ -21731,11 +22920,11 @@ Usage:
21731
22920
  pluxx init [name] [--from-mcp <source>] Create a new pluxx.config.ts
21732
22921
  pluxx sync [--from-mcp <source>] Refresh MCP-derived scaffold files
21733
22922
  pluxx migrate <path> Import an existing plugin into pluxx
21734
- pluxx test [--target <platforms...>] [--install] Run config, lint, eval, build, and smoke checks
22923
+ pluxx test [--target <platforms...>] [--install] [--behavioral] Run config, lint, eval, build, and smoke checks
21735
22924
  pluxx eval Evaluate scaffold and prompt-pack quality
21736
22925
  pluxx install [--target <platforms>] [--trust] Install built plugins for local testing
21737
22926
  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]
22927
+ pluxx publish [--npm] [--github-release] [--allow-dirty] [--dry-run] [--json] [--tag latest] [--version x.y.z]
21739
22928
  pluxx uninstall [--target <platforms>] Remove symlinked plugins
21740
22929
  pluxx help Show this help
21741
22930
 
@@ -21744,6 +22933,7 @@ Common flags:
21744
22933
  --quiet Suppress non-error chatter
21745
22934
  --verbose-runner Stream runner stdout/stderr for agent run/autopilot
21746
22935
  --dry-run Show planned work without writing files or installing anything
22936
+ --allow-dirty Skip the clean-working-tree check for publish planning or CI release flows
21747
22937
  --mode quick|standard|thorough Control how much agent refinement autopilot performs
21748
22938
  --approve-mcp-tools Preapprove all tools from the imported MCP in canonical permissions
21749
22939
  --ingest-provider auto|local|firecrawl Choose the docs/website ingestion backend for agent prepare/autopilot
@@ -21753,6 +22943,9 @@ Targets:
21753
22943
  warp, gemini-cli, roo-code, cline, amp
21754
22944
 
21755
22945
  Examples:
22946
+ pluxx --version Print the installed CLI version
22947
+ pluxx upgrade Upgrade the global npm install to latest
22948
+ pluxx upgrade --version x.y.z Upgrade the global npm install to a specific version
21756
22949
  pluxx build Build for all configured targets
21757
22950
  pluxx build --install Build and install all configured targets locally
21758
22951
  pluxx build --target claude-code cursor Build for specific platforms
@@ -21792,6 +22985,8 @@ Examples:
21792
22985
  pluxx eval --json Inspect scaffold/prompt-pack quality as JSON
21793
22986
  pluxx test --target claude-code codex Verify selected target outputs
21794
22987
  pluxx test --install Verify and install all configured targets locally
22988
+ pluxx test --install --trust --behavioral Run installed headless example-query smoke checks
22989
+ pluxx test --install --trust --behavioral --behavioral-prompt "Use My Plugin to summarize this repo"
21795
22990
  pluxx install Install to all configured targets
21796
22991
  pluxx install --target claude-code Install to Claude Code only
21797
22992
  pluxx verify-install --target codex Verify the installed Codex bundle in its native local path
@@ -21801,7 +22996,7 @@ Examples:
21801
22996
  }
21802
22997
  if (import.meta.main) {
21803
22998
  main().catch((err) => {
21804
- console.error(err);
22999
+ console.error(err instanceof Error ? err.message : err);
21805
23000
  process.exit(1);
21806
23001
  });
21807
23002
  }