@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/README.md +66 -3
- package/dist/cli/behavioral.d.ts +27 -0
- package/dist/cli/behavioral.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1667 -472
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/primitive-summary.d.ts.map +1 -1
- package/dist/cli/publish.d.ts +1 -0
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/commands.d.ts +3 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts +2 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +1 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/generators/cursor/index.d.ts.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/opencode/index.d.ts +1 -0
- package/dist/generators/opencode/index.d.ts.map +1 -1
- package/dist/generators/shared/claude-family.d.ts +1 -0
- package/dist/generators/shared/claude-family.d.ts.map +1 -1
- package/dist/index.js +200 -80
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/schema.d.ts +707 -707
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +4 -3
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.
|
|
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
|
-
|
|
4195
|
-
|
|
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
|
|
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
|
|
4870
|
-
if (!
|
|
4871
|
-
this.copyDir(configPath, `${
|
|
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
|
|
4949
|
-
import { resolve as
|
|
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
|
-
|
|
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 =
|
|
5153
|
-
if (!
|
|
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
|
|
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 (!
|
|
5201
|
-
const current =
|
|
5406
|
+
if (!existsSync7(outputPath)) continue;
|
|
5407
|
+
const current = readFileSync3(outputPath, "utf-8");
|
|
5202
5408
|
const hiddenName = buildHiddenSkillName(skill.effectiveName);
|
|
5203
|
-
const rewritten =
|
|
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 (!
|
|
5497
|
+
if (!existsSync7(commandsSrc) || !existsSync7(skillsSrc)) return [];
|
|
5214
5498
|
const commandNames = collectTopLevelCommandNames(commandsSrc);
|
|
5215
5499
|
const collidingSkills = [];
|
|
5216
|
-
for (const entry of
|
|
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 (!
|
|
5220
|
-
const content =
|
|
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
|
|
5535
|
+
for (const entry of readdirSync2(commandsRoot, { withFileTypes: true })) {
|
|
5232
5536
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
5233
|
-
commandNames.add(
|
|
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
|
|
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: ${
|
|
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: ${
|
|
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
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
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
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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: {
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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", "
|
|
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
|
|
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
|
|
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"
|
|
6982
|
-
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,
|
|
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
|
|
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: ["
|
|
7017
|
-
required:
|
|
7018
|
-
notes: "OpenCode plugins are loaded as local modules or npm packages rather than a
|
|
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: ["
|
|
7473
|
+
files: ["opencode.json"],
|
|
7474
|
+
rootKey: "mcp",
|
|
7022
7475
|
transports: ["local", "remote"],
|
|
7023
|
-
auth: ["headers", "
|
|
7024
|
-
notes:
|
|
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: ["
|
|
7034
|
-
format: "
|
|
7035
|
-
notes: "
|
|
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
|
|
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,
|
|
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: ["
|
|
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 =
|
|
8196
|
+
const expectedDirName = basename4(dirname3(skillFile));
|
|
7714
8197
|
const platformsRequiringDirMatch = targets.filter((t2) => PLATFORM_LIMITS[t2].skillNameMustMatchDir);
|
|
7715
|
-
|
|
7716
|
-
|
|
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:
|
|
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: "
|
|
8974
|
+
platform: "codex"
|
|
8308
8975
|
});
|
|
8309
8976
|
}
|
|
8310
|
-
if (
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
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
|
|
8327
|
-
if (!config.targets.includes("
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
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
|
|
8432
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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: ${
|
|
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" ?
|
|
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
|
|
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((
|
|
11806
|
+
const endpointReady = new Promise((resolve24, reject) => {
|
|
11142
11807
|
resolveEndpoint = (value) => {
|
|
11143
11808
|
endpointSettled = true;
|
|
11144
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
13301
|
-
const
|
|
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
|
|
13567
|
-
source.description,
|
|
13568
|
-
...source.paragraphs ?? []
|
|
13569
|
-
]
|
|
13570
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
|
13602
|
-
return value.split(/(?<=[.!?])\s+/).map(
|
|
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.
|
|
13660
|
-
6.
|
|
13661
|
-
7.
|
|
13662
|
-
8.
|
|
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
|
|
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
|
|
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," : ","}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14873
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
16163
|
-
import { existsSync as existsSync24, readdirSync as readdirSync10, mkdirSync as
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
17223
|
-
const index =
|
|
18075
|
+
function readOption(rawArgs2, flag) {
|
|
18076
|
+
const index = rawArgs2.indexOf(flag);
|
|
17224
18077
|
if (index === -1) return void 0;
|
|
17225
|
-
const value =
|
|
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(
|
|
17232
|
-
const source = readOption(
|
|
17233
|
-
const recordPath = readOption(
|
|
17234
|
-
const replayPath = readOption(
|
|
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
|
-
|
|
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(
|
|
17423
|
-
return await runMcpProxyWithIo(
|
|
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(
|
|
18282
|
+
async function runMcpProxyWithIo(rawArgs2, io) {
|
|
17430
18283
|
let options;
|
|
17431
18284
|
try {
|
|
17432
|
-
options = parseOptions(
|
|
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((
|
|
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
|
-
|
|
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
|
|
18462
|
-
import { mkdir as mkdir4, mkdtemp as
|
|
18463
|
-
import { tmpdir as
|
|
18464
|
-
import { spawn as
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
19379
|
-
jsonOutput:
|
|
19380
|
-
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(
|
|
19387
|
-
return
|
|
20240
|
+
function readFlag(rawArgs2, flag) {
|
|
20241
|
+
return rawArgs2.includes(flag);
|
|
19388
20242
|
}
|
|
19389
|
-
function readOption2(
|
|
19390
|
-
const index =
|
|
20243
|
+
function readOption2(rawArgs2, flag) {
|
|
20244
|
+
const index = rawArgs2.indexOf(flag);
|
|
19391
20245
|
if (index === -1) return void 0;
|
|
19392
|
-
const value =
|
|
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(
|
|
19399
|
-
const index =
|
|
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 <
|
|
19403
|
-
const value =
|
|
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
|
|
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(
|
|
19837
|
-
const values = readMultiValueOption(
|
|
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 =
|
|
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 =
|
|
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(
|
|
21326
|
+
function parseInitFromMcpOptions(rawArgs2, initialName, initialSource) {
|
|
20161
21327
|
return {
|
|
20162
|
-
source: initialSource ?? readOption2(
|
|
20163
|
-
assumeDefaults:
|
|
20164
|
-
name: readOption2(
|
|
20165
|
-
author: readOption2(
|
|
20166
|
-
displayName: readOption2(
|
|
20167
|
-
targets: readOption2(
|
|
20168
|
-
docsUrl: readOption2(
|
|
20169
|
-
websiteUrl: readOption2(
|
|
20170
|
-
contextPaths: readMultiValueOption(
|
|
20171
|
-
ingestProvider: readOption2(
|
|
20172
|
-
authEnv: readOption2(
|
|
20173
|
-
authType: readOption2(
|
|
20174
|
-
authHeader: readOption2(
|
|
20175
|
-
authTemplate: readOption2(
|
|
20176
|
-
runtimeAuth: readOption2(
|
|
20177
|
-
oauthWrapper:
|
|
20178
|
-
approveMcpTools:
|
|
20179
|
-
grouping: readOption2(
|
|
20180
|
-
hooks: readOption2(
|
|
20181
|
-
transport: readOption2(
|
|
20182
|
-
jsonOutput:
|
|
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) :
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
}
|